Текст
                    
Fluent React Build Fast, Performant, and Intuitive Web Applications Tejas Kumar Foreword Ьу Kent С. Dodds Beijing • Boston. Farnham • Sebastopol • Tokyo o·REILLY.

Тед>КОС l(умар React К вершинам мастерства Создание быстрых, производительных и интуитивно понятных веб-приложений Астана «АЛИСТ» 2025
УДК ББК 004.438 32.973.26-018.1 К90 Кумар Т. К90 К вершинам мастерства: Пер. с англ. -Астана: АЛИСТ, React. 368 2025. - с.: ил. ISBN 978-601-08-4834-4 Рассматривается разработка веб-интерфейсов и веб-приложений с использова­ нием React - популярной библиотеки JavaScript для создания пользовательских интерфейсов. Рассмотрены фундаментальные концепции сис JSX, виртуальный DOM, React, такие как синтак­ алгоритм согласования и передовые методы оптими­ зации. Благодаря простым и наглядным примерам, книга помогает понять работу React как на базовом, так и на продвинутом уровне, оптимизировать код и исполь­ зовать различные механизмы React. Книга поможет овладеть навыками написания интуитивно понятного кода и максимально эффективно использовать все возмож­ ности React для создания современных веб-приложений. Для программистов ББК УДК 004.438 32.973.26-018.1 © 2025 ALIST LLP Authorized Russian translation ofthe English edition of Fluent React, ISBN 9781098\38714 © 2024 Tejas Kumar. This translation is puЬ\ished and sold Ьу perrnission ofO'Reilly Media, Inc., which owns or controls all rights to puЬ\ish and sell the same. Авторизованный перевод с английского языка на русский издания Fluent React, ISBN 9781098\38714 © 2024 Tejas Kurnar. Перевод опубликован и продается с разрешения компании-правообладателя ISBN 978-1-098-13871-4 (англ.) ISBN 978-601-08-4834-4 (каз.) O'Reilly Media, lnc. © Tejas Kumar, 2024 © Издание на русском языке. ТОО "АЛИСТ". 2025
Содержание Отзывы о книге "React. К вершинам мастерства" ................................................ 11 Предисловие ................................................................................................................... 13 Введение .......................................................................................................................... 15 У славные обозначения и соглашения ........................................................................... 16 Платформа онлайн-обучения O'Reilly .......................................................................... 17 Как с нами связаться? ..................................................................................................... 17 Благодарности ................................................................................................................. 18 ГЛАВА 1. Обзор для начинающих ............................................................................. 21 Почему React - это стоящая вещь? .............................................................................. 21 React .................................................................................................................... 22 Библиотека jQuery .................................................................................................... 31 Библиотека Backbone ............................................................................................... 34 Шаблон MVC ........................................................................................................... 35 Библиотека КnockoutJS ........................................................................................... 40 Библиотека AngularJS .............................................................................................. 45 Введение в React .............................................................................................................. 50 Ценное предложение, внесенное React .................................................................. 51 Выпуск React ............................................................................................................ 57 Архитектура Flux ..................................................................................................... 58 Преимущества архитектуры Flux ........................................................................... 60 Подведение итогов: почему React- это стоящая вещь? ................................ :........... 61 Обзор главы ..................................................................................................................... 62 Проверьте ваши знания .................................................................................................. 62 Что дальше? ..................................................................................................................... 62 Мир до ГЛАВА 2. JSX ................................................................................................................. 63 Является ли JSX симбиозом JavaScript и XML? .......................................................... 63 JSX .......................................................................................................... 66 Преимущества
6 1 Содержание JSX ............................................................................................................... 67 ......................................................................................................... 68 Как работает код? .................................................................................................... 68 Расширение синтаксиса JavaScript с помощью JSX ............................................. 72 JSХ-прагма ....................................................................................................................... 74 Выражения ....................................................................................................................... 75 Обзор главы ..................................................................................................................... 76 Проверьте ваши знания .................................................................................................. 76 Что дальше? ..................................................................................................................... 76 Недостатки Что "под капотом"? ГЛАВА 3. Виртуальный DOM .................................................................................... 77 DOM ...................................................................................... 77 Реальный DOM ................................................................................................................ 78 Подводные камни реального DOM ........................................................................ 83 Фрагменты документа ............................................................................................. 91 Как работает виртуальный DOM ................................................................................... 93 Rеасt-элементы ......................................................................................................... 94 Сравнение виртуального DOM и реального DOM ............................................... 98 Эффективные обновления ..................................................................................... 100 Обзор главы ................................................................................................................... 102 Проверьте ваши знания ................................................................................................ 103 Что дальше? ................................................................................................................... 103 Введение в виртуальный ГЛАВА 4. Внутри согласования ............................................................................... 105 ........................................................................ 105 107 Небольшой экскурс в историю .................................................................................... 109 Стековое согласование (наследие) ....................................................................... 109 FiЬеr-согласователь ....................................................................................................... 112 Fiber как структура данных ................................................................................... 113 Двойная буферизация ............................................................................................ 115 FiЬеr-согласование ................................................................................................. 116 Обзор главы ................................................................................................................... 122 Проверьте ваши знания ................................................................................................ 122 Что дальше? ................................................................................................................... 122 Разбираемся в процессе согласования Пакетная обработка ....................................................................................................... ГЛАВА 5. Общие вопросы и мощные шаблоны Запоминание с помощью ................................................... 123 React.memo ......................................................................... React.memo ........................................................................... Сохраненные компоненты, которые отрисовываются повторно ...................... Это рекомендация, а не правило ..................................................... :.................... Как быстро освоить 123 127 127 131
Содержание 1 7 useMemo .............................................................................. 137 139 Забудьте обо всем этом ......................................................................................... 146 Ленивая загрузка ........................................................................................................... 147 Улучшенный контроль UI с помощью Suspense ................................................. 151 Сравнение useState и useReducer ................................................................................. 153 Immer и эргономика ............................................................................................... 157 Мощные паттерны ........................................................................................................ 159 Компоненты презентации и контейнера .............................................................. 160 Компонент более высокого порядка .................................................................... 162 Пропсы рендеринга ................................................................................................ 170 Паттерн Control Props ............................................................................................ 172 Коллекции пропсов ................................................................................................ 174 Составные компоненты ......................................................................................... 176 Паттерн State Reducer ............................................................................................ 180 Обзор главы ................................................................................................................... 183 Проверьте ваши знания ................................................................................................ 183 Что дальше? ................................................................................................................... 184 Запоминание с помощью useMemo ГЛАВА 6. считается вредным ................................................................................. Серверный React ...................................................................................... 185 185 SEO .......................................................................................................................... 185 Производительность .............................................................................................. 186 Безопасность ........................................................................................................... 189 Развитие серверного рендеринга ................................................................................. 192 Преимущества серверного рендеринга ................................................................ 192 Гидратация ..................................................................................................................... 193 Считается ли вредной гидратация? ...................................................................... 194 Организация серверного рендеринга .......................................................................... 195 Ограничения клиентского рендеринга ........................................................................ Ручное добавление в клиентское Rеасt-приложение серверного 196 Гидратирование ...................................................................................................... 198 API для серверного рендеринга в React ...................................................................... 198 renderToString ......................................................................................................... 199 renderToPipeaЬ/eStream ......................................................................................... 202 renderToReadaЬ/eStream ........................................................................................ 214 Когда какой API использовать? ............................................................................ 215 Стоит ли изобретать велосипед? ................................................................................. 216 Обзор главы ................................................................................................................... 220 Проверьте ваши знания ................................................................................................ 220 Что дальше? ................................................................................................................... 221 рендеринга ..............................................................................................................
8 1 Содержание ГЛАВА 7. Конкурентный React ................................................................................ 223 Проблема синхронного рендеринга ............................................................................ 224 Возвращаясь к Fiber ...................................................................................................... 225 Планирование и отсрочка обновлений ........................................................................ 225 Погружаемся глубже ..................................................................................................... 228 Планировщик ......................................................................................................... 229 Полосы рендеринга ....................................................................................................... 233 Как работают полосы рендеринга ........................................................................ 235 Обработка полос .................................................................................................... 236 Фаза фиксации ........................................................................................................ 236 useTransition ................................................................................................................... 237 Простой пример ..................................................................................................... 238 Более сложный пример: навигация ...................................................................... 239 Погружаемся глубже ............................................................................................. 241 useDeferredValue ............................................................................................................ 241 Цель использования useDeferredValue ................................................................. 243 Когда применять useDeferredValue ...................................................................... 245 Когда не следует использовать useDeferredValue ............................................... 246 Проблемы с конкурентным рендерингом ................................................................... 24 7 Разрыв ..................................................................................................................... 247 Обзор главы ................................................................................................................... 256 Проверьте ваши знания ................................................................................................ 257 Что дальше? ................................................................................................................... 258 ГЛАВА 8. Фреймворки .............................................................................................. 259, Зачем нам нужен фреймворк ........................................................................................ 259 Сер верный рендеринг ............................................................................................ 263 Маршрутизация ...................................................................................................... 264 Выборка данных ..................................................................................................... 266 Преимущества использования фреймворка ................................................................ 270 Компромиссы использования фреймворка ................................................................. 2 71 Популярные фреймворки React ................................................................................... 272 Remix ....................................................................................................................... 272 Next.js ...................................................................................................................... 282 Выбор фреймворка ........................................................................................................ 290 Понимание потребностей вашего проекта .......................................................... 290 Next.js ...................................................................................................................... 291 Remix ....................................................................................................................... 292 Компромиссы ......................................................................................................... 292 Опыт разработчиков .............................................................................................. 293 Производительность во время выполнения ......................................................... 294
Содержание 1 9 ................................................................................................................... 295 ................................................................................................ 295 Что дальше? ................................................................................................................... 296 Обзор главы Проверьте ваши знания ГЛАВА 9. Серверные компоненты Преимущества React ............................................................... 297 ............................................................................................................... 299 Сер верный рендеринг ................................................................................................... 299 ........................................................................................................ 302 Внесение обновлений ............................................................................................ 311 Нюансы ................................................................................................................... 316 Правила работы сер верных компонентов ................................................................... 317 Главное - сериализуемость ................................................................................. 317 Отсутствие эффективных хуков ........................................................................... 318 Состояние на самом деле не является состоянием ............................................. 318 Клиентские компоненты не могут импортировать серверные компоненты .... 318 Клиентские компоненты не так уж плохи ........................................................... 320 Серверные действия ...................................................................................................... 320 Формы и мутации .................................................................................................. 321 За пределами форм ................................................................................................ 321 Будущее серверных компонентов React ..................................................................... 323 Обзор главы ....................................................................... , ........................................... 323 Проверьте ваши знания ................................................................................................ 324 Что дальше? ................................................................................................................... 324 "Под капотом" ГЛАВА 10. Альтернативы React .............................................................................. 327 Vue.js .............................................................................................................................. 327 Сигналы .................................................................................................................. 329 Простота ................................................................................................................. 330 Angular ........................................................................................................................... 330 Обнаружение изменений ....................................................................................... 330 Сигналы .................................................................................................................. 331 Svelte ............................................................................................................................... 332 Руны ........................................................................................................................ 333 Solid ................................................................................................................................ 337 Qwik ................................................................................................................................ 338 Общие шаблоны ............................................................................................................ 340 Архитектура, основанная на компонентах .......................................................... 340 Декларативный синтаксис ..................................................................................... 340 Обновления ............................................................................................................. 341 Методы жизненного цикла ................................................................................... 341 Экосистема и инструменты ................................................................................... 341
1О I Содержание React не значит реактивный ......................................................................................... 342 Пример: зависимые значения ............................................................................... 345 Будущее React ................................................................................................................ 347 React Forget ............................................................................................................. 349 Обзор главы ................................................................................................................... 350 Проверьте ваши знания ................................................................................................ 351 Что дальше? ................................................................................................................... 352 ГЛАВА 11. Заключение .............................................................................................. 353 Итоговые выводы .......................................................................................................... 353 Этапы нашего пути ....................................................................................................... 3 55 Механика, лежащая в основе волшебства .................................................................. 356 Расширенные возможности .......................................................................................... 357 Будьте в курсе последних событий ............................................................................. 357 Предметный указатель .............................................................................................. 361 Об авторах .................................................................................................................... 365 Об изображении на обложке ..................................................................................... 367
Отзывы о книге "React. К вершинам мастерства" Карл Саган однажды сказал: "Вы должны знать прошлое, чтобы понять настоящее". По моему скромному мнению, эта книга не что иное, как попытка Теджаса привне­ сти эту мантру в сферу фронтенда, прокладывая для нас путь через историю React. С тщательной детализацией он дает подробный обзор ключевых концепций, лежа­ щих в основе согласования и микрофреймворка Fiber. Это обязательная к прочте­ React и его постоянно развиваю­ нию книга для всех, кто стремится глубже понять щуюся экосистему. - Матеус Альбукерке Бразw,, эксперт-разработчик Google по веб-технологии Эта книга заставила меня усомниться во всем том, что, как мне казалось, я знал о React! Это именно тот контент, которого ожидали от Теджаса: подробное описание путешествия по истории и внутреннему устройству вы хотите расширить свои знания о React, React, понятное каждому. Если то эта книга станет вашим новым луч­ шим другом! Это самое подробное объяснение Даниэль Афонсо, девелопер-адвокат, OLX React и того, как он работает, которое я когда­ React стоит прочитать эту книгу, чтобы изу­ либо встречал. Каждому разработчику чить фреймворк изнутри. И я говорю это не ради красивой похвалы! Книга Теджаса Кумара "React. Сергей Кирьянов, девелопер-адвокат, Vue Storefront К вершинам мастерства" не только углубила мое понимание сложных концепций, таких как гидратация, мемоизация и серверные компоненты, но и отточила мою способность разрабатывать стратегию поведения моего кода, что улучшает процесс разработки еще до того, как будет написана пер­ вая строка. - Кеннет Квиггинс, разработчик программного обеспечения
12 Отзывы о книге "React. К вершинам мастерства" В отличие от большинства книг по пользовать React, React, которые учат вас только тому, как ис­ эта книга рассказывает о том, как работает React. Теджас проде­ лал огромную работу, чтобы охватить сложные детали, которые большинство дру­ гих авторов просто опускают. И не только это, но и то, что он поставил себя в уязвимое положение, чтобы некоторые из величайших представителей сообщества оценили его работу. Он учился у них и вернул свои новые знания в книгу. Огром­ ное ему спасибо! специалист по сопровождению.Ароllо Ленц Вебер- Троник, Client & Redux Too/kit
Предисловие Веб-разработчику необходимо многое знать и понимать, чтобы создавать приложе­ ния, которые понравятся пользователям. В частности, в области React вам доступно огромное количество материалов. Как ни странно, это является частью проблемы. Далеко не все материалы согласуются друг с другом, и вам придется прокладывать собственный путь, просматривая все доступные учебные пособия и записи в благах в надежде составить учебную программу, свободную от противоречий и зияющих пробелов. И вы всегда будете беспокоиться о том, что то, чему вы учитесь, уже ус­ тарело. Вот тут-то и появляется Теджас Кумар с этой книгой. Теджас имеет многолетний опыт работы в React и приложил немало усилий, чтобы углубиться в темы, которые дадут вам прочную основу знаний. Он опытный инженер и кладезь знаний, которые помогут вам почувствовать себя уверенно в React. Мне посчастливилось сотрудни­ чать с Теджасом на протяжении многих лет в различных проектах, Я следил за его выступлениями на конференциях, за тем, как он создавал образовательный контент. Я хорошо знаю его на личном уровне. Я надеюсь, вы быстро поймете, что у вас в руках книга, написанная талантливым специалистом, который к тому же еще и пре­ красный человек. В этой книге вы познакомитесь с темами, в которые иначе не смогли бы, скорее всего, погрузиться, и которые помогут вам "мыслить в стиле React", используя пра­ вильную ментальную модель. Вы, в первую очередь, придете к пониманию цели существования React, что даст вам хорошую основу для рассмотрения инструмента для решения ваших задач. React React как был изобретен не в вакууме, и зна­ комство с историей его возникновения поможет вам понять, какие проблемы при­ зван решать React, чтобы вам не пришлось вставлять квадратный колышек в круг­ лое отверстие. Вы поймете основные концепции, такие как и конкурентный React, JSX, виртуальный DOM, согласование которые помогут вам использовать этот инструмент более эффективно. Я всегда считал, что лучший способ повысить свой опыт работы с ин­ струментом зуете React - это понять, как он работает. Таким образом, даже если вы исполь­ уже много лет, главы этой книги откроют вам глаза на новые возмож­ ности, поскольку вы начнете по-настоящему понимать React, а не просто собирать все воедино и надеяться (скрестив пальцы) на положительный результат.
Предисловие 14 Вы познакомитесь с шаблонами, которые используют профессионалы для создания эффективных и действенных абстракций. React сам по себе является очень быстрой библиотекой пользовательского интерфейса, но иногда при создании сложных при­ ложений требуется оптимизировать производительность. Теджас покажет вам, как это сделать с помощью методов запоминания, ленивой загрузки 1 и управления со­ стоянием, которые доступны в React. Кроме того, вы поймете, как лучшие библио­ теки экосистемы работают с такими паттернами (шаблонами), как Compound ComControl Props. Это важнейшие приложения React. Даже если вы про­ ponents, Render Props, Prop Getters, State Reducers инструменты в вашем арсенале при создании и сто используете готовые решения, понимание того, как работают эти мощные абст­ ракции, поможет вам использовать их более эффективно. Однако Теджас выходит за рамки теоретического React, и вы познакомитесь с Next.js. Это будет важно для вас, чтобы научиться пользоваться всеми возможностями React для формирования наилучшего пользовательского опыта. Возможности серверного рендеринга React практическими фреймворками, такими как Remix и позволяют вам с самого начала управлять разработкой, поскольку вы используете React ме для создания как пользовательского интерфейса, так и серверной части. Кро­ того, вы познакомитесь с передовыми технологиями, такими как серверные компоненты (React Server Components, RSC) и серверные действия (React Server Actions, RSA), которые React использует для улучшения взаимодействия с пользо­ вателем. Я уверен, что после изучения материала, который Теджас собрал для вас, вы будете иметь знания, необходимые для создания исключительных приложений с помощью React. Я желаю вам всего наилучшего на вашем пути работы с React, самой широко используемой библиотекой пользовательского интерфейса в мире. Наслаждайтесь путешествием! Кент Доддс (Kent С. Dodds), http://kentdodds.com 1 Ленивая загрузка (lazy loading) - возможность отложить загрузку ненужного прямо сейчас контента. Ленивая загрузка делает сайт быстрее и экономит трафик, если у пользователя мобильный Интернет с ограничениями. - Прим. пер.
Введение Эта книга не для тех, кто хочет научиться использовать с React React. Если вы не знакомы и ищете хорошее учебное пособие, отличное место для начала- докумен­ тация по React на сайте react.dev (https://react.dev/). Напротив, эта книга предна­ значена для любознательных: людей, которым не так интересно, как использовать React, но которых больше интересует, как работает React. За время, проведенное вместе, мы познакомимся с рядом концепций и пой­ React мем лежащий в их основе механизм. Мы изучим, как все это сочетается друг с дру­ гом, с целью научиться эффективно создавать приложения с использованием В нашем стремлении понять лежащий в основе механизм мы React. разработаем ментальные модели1, необходимые для того, чтобы с высокой степенью точности представлять React и его экосистему. В этой книге предполагается, что вы достаточно хорошо понимаете следующее ут­ верждение: браузеры отображают веб-страницы. документы, оформленные с помощью CSS Веб-страницы - это НТМL­ и имеющие интерактивность благодаря JavaScript. Также предполагается, что вы немного знакомы с тем, как использовать React, и уже имеете опыт создания одного или двух приложений React. Будет иде­ ально, если некоторые ваши приложения React находятся в процессе разработки. Мы начнем с введения в мыслями в 2013 React и кратко расскажем о его истории, возвращаясь год, когда он был впервые выпущен в виде программного обеспе­ чения с открытым исходным кодом. Далее мы рассмотрим основные концепции React, включая компонентную модель, виртуальный и согласование. Мы по­ DOM знакомимся с теорией компиляторов, объясняющей, как работает JSX, поговорим о fiber-yзлax и подробно разберемся в модели параллельного программирования. Таким образом, мы получим полезные рекомендации, которые помогут нам легче запоминать то, что следует запомнить, и отложить работу по рендерингу, которую следует отложить, с помощью мощных примитивов, таких как React. 1'1е1'1О и useTrans'i.ti.on. 1 Ментальные модели (mental model) - основанные на предыдущем опыте идеи, стратегии, способы понимания, существующие в уме человека и направляющие его действия. Ментальные модели используются для объяснения причин и следствий, а также придания смысла жизненному опыту. Ментальные модели являются естественными и присущи каждому человеку вне зависимости от того, осознает он это или нет. Ментальные модели не являются неизменными. - Прим. пер.
Введение 16 Во второй половине этой книги мы рассмотрим фреймворки React: какие проблемы они решают и с помощью каких механизмов. Для этого мы напишем наш собствен­ ный фреймворк, который решает три основные задачи практически во всех веб­ приложениях: серверный рендеринг, маршрутизацию и получение данных клиентом. Как только мы решим эти задачи сами, понимание того, как фреймворки решают их, станет намного более доступным. Мы также углубимся в состав серверных компонентов React Server Components (RSC) и действия сервера 2 , понимание роли инструментов нового поколения, таких как бандлеры 3 и изоморфные маршрутиза­ торы. Наконец, мы немного отдалимся от Vue, Solid, Angular, Qwik React и рассмотрим альтернативы, такие как и др. Мы рассмотрим сигналы и детализированную реак­ тивность в сравнении с грубой моделью реактивности реакцию React React. Мы также рассмотрим Forget и использование на сигналы и сравним работу инструментов сигналов. Нам предстоит многое обсудить, так что больше не будем терять времени. Давайте начнем! Условные обозначения и соглашения В книге используются следующие общепринятые типографские обозначения. Курсивом Обозначены новые термины, имена файлов и каталогов, расширения файлов. Жирным шрифтом Обозначены URL-aдpeca, адреса электронной почты, элементы интерфейса про­ грамм. Моноwиринный wрифт Используется в листингах программ, а также внутри абзацев для обозначения таких элементов программы, как имена переменных и функций, базы данных, типы данных, переменные среды, операторы, ключевые слова. Жирt111й МОНО111иринt1о1й wрифт Обозначает ключевые слова в коде программ. Курсивный монО1JJuринный шрифР, Обозначает комментарии в коде программ, а также текст, который должен быть заменен значениями, введенными пользователем, или значениями, определяе­ мыми контекстом. 2 Действия сервера (server actions) предназначены для мутаций (изменений), которые обновляют состояние на стороне сервера. Фреймворки, реализующие Server Actions, обычно обрабатывают одно действие за раз и не имеют возможности кешировать возвращаемое значение. 3 Бандлер (Ьundler) программа, которая упаковывает сложный проект - При.11. пер. со многими файлами и внешними зависимостями в один (иногда несколько) файл, который будет отравлен браузеру. пер. - Прим.
Введение 1 17 ~ Данный шм,нт обоsнаsает общее щмеsание Платформа онлайн-обучения Более 40 O'Reilly лет компания O'Reilly Media организует технические и бизнес-тренинги, передает знания и опыт, чтобы помочь компаниям достичь успеха. Наша уникальная сеть экспертов и новаторов делится своими знаниями и опытом через книги, статьи и платформу онлайн-обучения. Эта платформа предоставляет доступ по требованию к интерактивным учебным курсам, углубленным програм­ мам обучения, интерактивным средам кодирования, а также к обширной коллекции текстовых и видеоматериалов от O'Reilly 200 других издательств. https://oreilly.com. и более нительная информация представлена на сайте Допол­ Как с нами связаться? Просим направлять комментарии и вопросы относительно данной книги в изда­ тельство по адресу: O'Reilly Media, Inc. 1005 Gravenstein Highway North Sebastopol, СА, 95472 800-998-9938 (в США или Канаде) 707-829-0515 (международный или местный) 707-829-0104 (факс) support@oreily.com https://www.oreily.com/about/contact.html На веб-странице книги можно ознакомиться с редакциями, примерами и другой дополнительной информацией. Она доступна по адресу Новости и информацию о наших книгах и https://oreil.ly/fluent-react. курсах можно найти https://oreilly.com. Ищите нас на Linkedln: https://linkedin.com/company/oreilly-media. Следите за нами в Смотрите нас на Twitter: https://twitter.com/oreillymedia. YouTube: https://www.youtube.com/oreillymedia. на сайте
Введение 18 Благодарности Эта книга первая книга, которую я когда-либо писал, и я безмерно благодарен - судьбе за то, что сделал это не в одиночку. То, что вы сейчас прочтете, является результатом совместных усилий нескольких замечательных людей, которые рабо­ тали сообща, чтобы выпустить книгу в свет. На этой странице мы выражаем при­ знательность людям за их вклад в подготовку данного текста. Пожалуйста, не пропустите это, потому что эти люди заслуживают вашего внима­ ния и благодарности. Давайте начнем с людей, которые непосредственно помогли мне с этой книгой. ♦ Для меня на первом месте всегда есть и будет моя жена Леа. Я потратил много времени на написание этой книги, часто ценой того, что мне не хватало времени побыть с ней вдвоем и провести время с семьей. Я рад тому, что эта книга появилась и позволила мне поделиться своим опытом со всеми вами, хотя работа над книгой съела часть моего отпуска и лишила многих возможностей провести время с моей женой. Она всегда поддерживала и ободряла меня, и я очень благодарен ей. ♦ Шира Эванс O'Reilly (Shira Evans), было мой редактор по разработке этой книги. С Широй из приятно работать, и она всегда поддерживала, ободряла и понимала нас, даже когда мы сталкивались с рядом задержек, поскольку в React постоянно появлялись новые вещи, такие как Шира Forget и Server actions. проявляла терпение, и я так благодарен ей за это. ♦ Я благодарен моему дорогому другу и брату Кенту К. Доддсу @kentcdodds) (Kent С. Dodds, за его постоянное наставничество, выходящее за рамки этой книги, а также за его предисловие к книге. Кент является моим дорогим другом и наставником на протяжении многих лет, и я очень признателен ему за постоянную поддержку и ободрение. ♦ Рецензенты. Эта книга была бы невозможна без невероятной тщательности и внимания к деталям со стороны рецензентов, которые сотрудничали со мной при работе над этой книгой: • Adam Rackis (@adamrack.is); • Daniel Afonso (@danieljcafonso); • Fabien Bemard (@fablen0102); • Kent С. Dodds (@kentcdodds); • Mark Erikson (@acemarke); • Lenz Weber-Tronic (@phry); • Rick Hanlon 11 (@rickhanlonii); • Sergeii Kirianov (@SergiiIOrianov); • Matheus Albuquerque (@ythecomblnator).
Введение ♦ Я благодарен команде React из Meta React React, за то, React и делают за их неустанную работу над что они продолжают расширять границы возможного с помощью использование 19 приятным благодаря своему таланту, изобретательности и инженерной хватке. В частности, Дэну Абрамову (Dan Abramov, @dan_abramov), React который нашел время, чтобы объяснить роль бандлера в архитектуре Server Components, а также за значительный вклад в главу серверным компонентам 9, посвященную React. Наконец, я хотел бы поблагодарить вас, читатель, за проявленный интерес к этой книге. Надеюсь, вам будет так же приятно читать ее, как мне - писать.

ГЛАВА 1 Обзор для начинающих Давайте начнем с уточнения: React был создан для того, чтобы им могли пользо­ ваться все. На самом деле вы могли бы прожить всю жизнь, так и не прочитав эту книгу, и при этом использовать знакомит с React React без проблем! Эта книга гораздо глубже по­ тех из вас, кому интересны лежащий в основе данной технологии механизм, а также продвинутые паттерны и лучшие практики. Она лучше подходит React, а не для изучения того, как использовать React. Существует множество других книг, написанных с целью научить использо­ вать React на уровне конечного пользователя. В отличие от них, данная книга по­ может вам разобраться в React на уровне создателя библиотеки или фреймворка. для понимания того, как работает В соответствии с этой целью давайте вместе углубимся в изучение, начав сверху: с того, что относится к более высокому, начальному уровню. Мы начнем с основ React, React. а затем будем все глубже и глубже погружаться в детали того, как работает В этой главе мы поговорим о том, зачем существует React, как он работает и какие задачи решает. Мы расскажем о его возникновении и первоначальном дизайне и проследим за его развитием от скромного появления в Facebook 1 до распространен­ ного решения, которым он является сегодня. Эта глава является своего рода мета­ главой (это не шутка), потому что важно понять контекст React, прежде чем мы уг­ лубимся в детали. Почему React - это стоящая вещь? Ответ заключен в одном слове - обновления. На заре развития Интернета у нас было много статичных страниц. Мы заполняли формы, нажимали кнопку Отпра­ вить и в ответ загружали совершенно новую страницу. Какое-то время нас это уст­ раивало, но со временем потенциал веб-приложений значительно расширился. По мере роста возможностей росло и наше стремление к повышению качества работы пользователей в Интернете. Мы хотели, чтобы информация обновлялась мгновен­ но, без ожидания загрузки и отображения новой страницы. Мы хотели, чтобы стра- 1 На территории РФ деятельность социальной сети Facebook признана •➔ кстремистской, - Прим. ред.
22 Глава 1 ницы менялись быстрее, а веб-сайт обновлялся мгновеюю. Проблема заключалась в том, что эти мгновенные обновления было довольно сложно осуществить в боль­ ших масштабах по ряду причин. Производительность. Внесение обновлений на веб-страницы часто приводило к снижению производи­ тельности, поскольку мы заставляли браузеры пересчитывать макет страницы (делать так называемое переформатирование) и перерисовывать страницу заново. Надежность. Отслеживать состояние ресурса (веб-приложения) и следить за тем, чтобы он поддерживался в широком веб-пространстве, было сложно, потому что нам при­ ходилось контролировать состояние в нескольких местах и следить за тем, что­ бы оно было согласованным. Это было особенно трудно сделать, когда над од­ ной и той же кодовой базой работало несколько человек. Безопасность. Мы должны были обязательно санировать весь HTML и JavaScript, которые мы выводили на страницу, чтобы обезопасить себя от таких эксплойтов, как меж­ сайтовый скриптинг (cross-site scripting, XSS) (cross-site request forgery, CSRF). и подделка межсайтовых запросов Для того чтобы полностью понять и по достоинству оценить, как React решает эти проблемы для нас, нам нужно осмыслить контекст, в котором был создан показать "обстановку" до появления Мир до React. React, и Давайте сделаем это сейчас. React Для разработчиков веб-приложений до эры React существовало несколько серьез­ ных проблем. Нам нужно было придумать, как сделать наши приложения мгновен­ ными, масштабируемыми на миллионы пользователей и надежно работающими. Например, давайте рассмотрим нажатие кнопки: когда пользователь нажимает на кнопку, мы хотим обновить пользовательский интерфейс с целью отразить, что кнопка была нажата. Нам нужно рассмотреть по крайней мере четыре различных состояния, в которых может находиться пользовательский интерфейс. Не нажата. Кнопка находится в состоянии по умолчанию и не была пока нажата. Нажата, но ожидает подтверждения. Кнопка нажата, но действие, которое она должна выполнить, еще не закончено. Нажата, и действие успешно выполнено. Кнопка нажата, и действие, которое она должна выполнять, завершено. После этого мы можем вернуть кнопке состояние, в котором она была до нажатия, или изменить цвет кнопки (на зеленый), чтобы указать на успешное выполнение.
Обзор для начинающих 1 23 Нажата, но действие не выполнено. Кнопка была нажата, но действие, которое она должна произвести, не выполне­ но. После этого мы можем вернуть кнопке состояние, в котором она была до нажатия, или изменить цвет кнопки (на красный), чтобы указать на сбой. Как только мы получим новое состояние, нам нужно выяснить, как обновить поль­ зовательский интерфейс, чтобы отразить это. Часто для обновления интерфейса требуются следующие шаги: 1. Найдите кнопку на странице приложения (часто в браузере ), используя какой­ либо АРI-интерфейс для определения местоположения элементов, например docuP1ent. querySe lector или docuP1ent. getEleP1entByid. 2. Подключите к кнопке слушателя (листенер2 ), чтобы отслеживать события нажатия. 3. Выполняйте нужные обновления состояния в ответ на события. 4. Когда действие кнопки будет вьmолнено, удалите слушателя и очистите состояние. Это простой пример, но он хорош для начала. Допустим, у нас есть кнопка с надпи­ сью "Likе"("Нравится"), и когда пользователь нажимает на нее, мы хотим изменить надпись на "Liked" ("Понравилось"). Как нам это сделать? Начнем с того, что у нас будет НТМL-элемент: <Ьutton>Ltke</Ьutton> Нам нужен какой-то способ ссылаться на эту кнопку с помощью JavaScript, поэто­ му мы присвоим ей атрибут id: <button td="HkeButton">Ltke</button> Отлично! Теперь, когда есть идентификатор, JavaScript может работать с ним, что­ бы сделать кнопку интерактивной. Мы можем получить ссылку на кнопку, исполь­ зуя docuP1ent.getEleP1entByid, а затем добавим к кнопке слушателя для отслеживания событий нажатия: const HkeButton = docuP1ent.getEleP1entByid("HkeButton"); HkeButton.addEventListener("cHck", () => { // сделаем Чl'lо-нибудь }); Теперь, когда у нас есть слушатель, мы можем что-то делать при нажатии на кноп­ ку. Допустим, мы хотим обновить кнопку, чтобы при нажатии на нее появлялась надпись "Liked" ("Понравилось"). Мы можем сделать это, обновив текстовое со­ держимое кнопки: const HkeButton = docuP1ent.getEleP1entByid("HkeButton"); HkeButton.addEventlistener("cHck", () => { HkeButton.textContent = "Ltked"; }); 2 Листенер (от англ. listener- слушатель)- функция, которая ожидает определенное событие и отве­ чает на него.
Глава 24 1 Великолепно! Теперь у нас есть кнопка с надписью появляется надпись "Liked". "Like", и при нажатии на нее Проблема в том, что мы не можем отменить наш лайк. Давайте исправим это и обновим кнопку, чтобы на ней снова было написано если она нажата в состоянии "Liked". "Like", Нам нужно было бы добавить к кнопке какое­ то состояние, чтобы отслеживать, нажимали на нее ранее или нет. Мы можем сде­ лать это, добавив к кнопке атрибут data- li.ked: <button i.d=="li.keButton" data-li.ked=="false">Li.ke</button> Теперь, когда у нас есть этот атрибут, мы можем использовать его для отслежива­ ния того, была ли нажата кнопка. Мы можем обновлять текстовое содержимое кнопки на основе значения этого атрибута: const li.keButton == docuмent.getEleмentByid("li.keButton"); li.keButton.addEventli.stener("cli.ck", О==> { "true"; const li.ked == li.keButton.getAttri.bute("data-li.ked") li.keButton.setAttri.bute("data-li.ked", !li.ked); li.keButton.textContent == li.ked? "Li.ke" : "Li.ked"; }); Подождите, но мы просто меняем текстовое содержимое кнопки! На самом деле мы не сохраняем статус "Liked" в базе данных. Обычно для этого нам приходилось взаимодействовать с сервером через сеть, например, так: const li.keButton == docu111ent.getEleмentByid("li.keButton"); li.keButton.addEventli.stener("cli.ck", () ==> { vаг li.ked == li.keButton.getAttri.bute("data-li.ked") ====== "true"; // Посылаем запрос серверу через семь == new XМLHttpRequest(); xhr.open("POST", "/li.ke", tгue); vаг хhг xhr.setRequestНeader("Content-Type", "appli.cati.on/json;charset==UTF-8"); xhr.onload == function () { i.f (xhr.status >== 200 && xhr.status < 400) { // Успех! l i.keButton. setAttri.bute( "data- l i.ked", !l i.ked); "Li.ked li.keButton. textContent == li.ked ? Li.ke } etse { 11 11 : 11 ; / / Мы связались с целевым сервером, но он возврамил оошбку console.error( Server returned an 11 } }; еггог:", xhr.statusText);
Обзор для начинающих 1 25 xhr.onerror = functton () { // ПpouзO/JJl/a Ollluбкa соединения console.error("Network еггог."); }; xhr.send(JSON.stringify({ liked: !ltked })); }); Конечно, здесь мы используем XМLHttpRequest и var, чтобы соответствовать времени. React был выпущен как программное обеспечение с открытым исходным кодом в 2013 году, а более распространенная АРI-функция fetch была представлена в 2015 году. В промежутке между XМLHttpRequest и fetch у нас был фреймворк jQuery, который часто позволял абстрагироваться от некоторой сложности с помощью та­ ких примитивов, как $.ajax(), $.post() и т. д. Сегодня наш код выглядел бы примерно так: const HkeButton = docul'lent.getEleмentByid("HkeButton"); HkeВutton.addEventlistener("cHck", () => { "true"; const Hked = HkeButton.getAttribute("data-Hked") // Посылаем запрос серверу через cef'lь fetch("/Hke", { P1ethod: "POST", Ьоdу: JSON.stringify({ ltked: !ltked }), }) . then( О => { ltkeButton.setAttribute("data-liked", !ltked); HkeButton.textContent = Hked? "Like" : "Ltked"; }); }); Если не вдаваться в подробности, то суть в том, что мы общаемся по сети, но что делать, если сетевой запрос завершится ошибкой? Нам нужно будет обновить тек­ стовое содержимое кнопки, чтобы отразить сбой. Мы можем сделать это, добавив к кнопке атрибут <Ьutton data-faHed: td="HkeButton" data-Hked="false" data-faHed="false">Ltke</button> Теперь мы можем обновить текстовое содержимое кнопки на основе значения это­ го нового атрибута: const HkeButton = docuP1ent.getEleP1entByld("HkeButton"); HkeButton.addEventlistener("cHck", () => { "true"; const Hked = HkeButton.getAttribute("data-Hked") // Посылаем запрос fetch("/Hke", { серверу через cef'lь
Глава 26 1 l'lethod: POST body: JSON.stri.ngi.fy({ li.ked: !li.ked }), 11 11 , }) . then( О => { li.keButton.setAttri.bute( data-li.ked !li.ked); HkeButton. textContent = Hked ? Li.ke Li.ked }) . catch( О => { HkeButton.setAttri.bute( data-faHed tгue); HkeButton.textContent = Fai.led }); }); 11 11 , 11 11 11 11 11 11 : 11 11 ; , ; Нужно обработать еще один момент: процесс, когда кнопка нажата, но реакции еще нет. То есть состояние ожидания. Для того чтобы смоделировать это в коде, мы ус­ тановим еще один атрибут для кнопки в состоянии ожидания, добавив data-pendi.ng следующим образом: <button i.d= li.keButton data-pendi.ng= false data-Hked= false data-fai.led= false 11 11 11 11 11 11 11 11 > Li.ke </button> Теперь мы можем отключить эту кнопку, пока выполняется сетевой запрос, чтобы нетерпеливые многократные нажатия не ставили запросы в очередь и не приводили к гонке запросов и перегрузке сервера. Мы можем сделать это следующим образом: const HkeButton = docul'lent.getElel'lentByid( HkeButton HkeButton.addEventli.stener( cli.ck О=> { const Hked = HkeButton.getAttri.bute( data-Hked === true const i.sPendi.ng = HkeButton.getAttri.bute( data-pendi.ng === true 11 11 11 11 ) ; , 11 11 11 li.keButton.setAttri.bute( data-pendi.ng 11 HkeButton.setAttri.bute( 11 di.saЫed 11 , 11 , 11 true 11 11 11 , { 11 , 11 11 11 di.saЫed 11 // Посьиаем запрос серверу через се~ь fetch( /Hke l'lethod: POST body: JSON.stri.ngi.fy({ li.ked: !li.ked }), }) 11 ) ); ); ) 11 ; 11 11 ;
Обзор для начинающих 27 . then( О => { li.keButton.setAttri.bute( 11 data-li.ked 11 , !li.ked); li.keButton. textContent = li.ked ? 11 Li.ke 11 : 11 Li.ked 11 ; li.keButton.setAttri.bute( 11 di.saЫed 11 , null); }) .catch( О => { li.keButton.setAttri.bute( 11 data-fai.led 11 , li.keButton.textContent = 11 Fai.led 11 ; 11 true 11 ) ; }) .fi.nally(() => { l i.keButton. setAttri.bute( data-pendi.ng 11 11 , 11 fa lse 11 ) ; }); }); Мы также можем использовать такие мощные методы, как замедление антидребезг (debouncing), (throttling) и чтобы избавиться от избыточных или повторяющихся действий. ~ В качестве краткого отступления мы упомянем методы замедления и анти­ дребезга. Антидребезг задерживает выполнение функции до тех пор, пока не истечет заданное время с момента запуска последнего события (напри­ мер, пока пользователи не перестанут печатать, чтобы обработать уже вве­ денные ими данные), а замедление ограничивает выполнение функции не более чем одним запуском за каждый заданный интервал времени, гаран­ тируя, что она не будет выполняться слишком часто (например, обработка событий прокручивания страницы с заданным интервалом). Оба метода оптимизируют производительность за счет контроля скорости выполнения функций. Хорошо, теперь наша кнопка стала достаточно мощным элементом интерфейса и может обрабатывать несколько состояний. Однако некоторые вопросы еще остаются. ♦ Действительно ли состояние data-pendi.ng необходимо? Нельзя ли просто прове­ рить, отключена ли кнопка? Вероятно, нет, поскольку кнопка может быть от­ ключена по другим причинам, например, если пользователь не вошел в систему или у него нет разрешения на нажатие кнопки. ♦ Может быть, более разумно иметь атрибут состояния данных стояние данных может иметь одно из значений pendi.ng, li.ked data-state, unli.ked или где со­ вместо множества других атрибутов данных? Возможно, но тогда нам нужно было бы добавить большой блок swi.tch/case или аналогичный блок кода для обработки каждого случая. В конечном счете объем кода для обработки обоих подходов значителен: в любом случае мы все равно сталкиваемся со сложностью и много­ словием.
Глава 28 1 ♦ Как нам изолированно протестировать эту кнопку? Можем ли мы это сделать? ♦ Почему мы изначально пишем кнопку на HTML, а затем работаем с ней в JavaScript? Не было бы лучше, если бы мы могли просто создать кнопку на JavaScript с помощью docuмent. createEleмent( 'button' ), а затем добавить ее в ка­ честве дочернего элемента docuмent. appendChi.ld(li.keButton)? Это упростило бы тестирование и сделало бы код более автономным, но тогда нам пришлось бы отслеживать родительский код, если родительский элемент кнопки не docuмent. На самом деле, нам, возможно, придется отслеживать всех родителей на странице. React помогает нам решить некоторые из этих проблем, но не все: например, на во­ ( i.sPendi.ng, hasFai. led и т. д.) state), React не дает отве­ менее React помогает нам ре­ прос о том, разбить ли состояние на отдельные флаги или представить состояние одной переменной (например, та. На этот вопрос мы должны ответить сами. Тем не шить задачу масштабирования: мы можем создать множество интерактивных кно­ пок, и обновлять пользовательский интерфейс в ответ на события эффективным способом и с минимальным затратами в кратчайшие сроки. это воспроизводимым, декларативным, React производительным, позволяет сделать предсказуемым и на­ дежным способом. Более того, React помогает нам сделать состояние намного более предсказуемым, полностью управляя состоянием пользовательского интерфейса и выполняя ренде­ ринг на основе этого состояния. Это резко контрастирует с тем, когда состоянием владеет и управляет браузер, при этом состояние самого браузера может быть в значительной степени ненадежным из-за ряда факторов, таких как другие запущен­ ные на странице клиентские скрипты, а также расширения браузера, ограничения устройства и многие другие переменные и события. Наш пример с кнопкой Like - весьма простой, но очень неплох для начала. До сих пор мы видели, как можно использовать JavaScript, чтобы сделать кнопку интерак­ тивной, но это очень сложный процесс, если мы хотим сделать его хорошо: нам нужно найти кнопку в браузере, добавить слушателя событий, обновить текстовое содержимое кнопки и учесть множество дополнительных обстоятельств. Это боль­ шая работа, и она не очень масштабируема. А если бы у нас было много кнопок на странице? Что, если у нас много кнопок, которые должны быть интерактивными? Что, если у нас много кнопок, которые должны быть интерактивными, и нам нужно было бы обновлять пользовательский интерфейс в ответ на события? Будем ли мы использовать делегирование событий (или всплытие событий) и подключать слу­ шателя событий к вышестоящему объекту docuмent? Или мы должны подключать слушателя к каждой кнопке? Как указано в предисловии этой книги, предполагается, что мы достаточно хорошо следующее понимаем страницы - браузеры отображают веб-страницы. Веб­ это НТМL-документы, оформленные с использованием каскадных таблиц стилей JavaScript. утверждение: (CSS) и обладающие интерактивностью, добавленной с помощью Это прекрасно работало на протяжении десятилетий и продолжает рабо­ тать до сих пор, но создание современных веб-приложений, предназначенных для обслуживания огромного (думаю, миллионов) числа пользователей, требует значи-
Обзор для начинающих 1 29 тельного объема абстракции. Большая степень абстракции позволит сделать инте­ рактивность безопаснее и надежнее и минимизировать возможности для возникно­ вения ошибок. К сожалению, на примере кнопки Like становится ясно, что нам по­ надобится некоторая помощь в этом вопросе. Давайте рассмотрим другой пример, который немного сложнее, чем наша кнопка Like. Начнем с простого списка элементов. Допустим, у нас есть список, в который мы хотим добавить новый элемент. Мы могли бы сделать это с помощью НТМL­ формы, которая выглядит примерно так: <ul i.d="li.st-parent"></ul> <fom i.d="add-i.teri-foп11" acti.on="/api./add-i.te111" 111ethod="POST > <i.nput type="text" i.d="new-li.st-i.te111-laЬel" /> <Ьutton type="suЬP1i.t">Add Ite111</button> </fom> 11 JavaScript предоставляет нам доступ мента (document object model, DOM). к АРI-интерфейсам объектной модели доку­ Для тех, кто не в курсе, DOM- это модель структуры документа веб-страницы. Это дерево объектов в памяти, представляю­ щее элементы на вашей странице и дающее вам способы взаимодействия с ними с помощью JavaScript. Проблема в том, что DOM на устройствах пользователей по­ добны чужой планете: у нас нет возможности узнать, какие браузеры они исполь­ зуют, в каких сетевых условиях и в каких операционных системах (ОС) они рабо­ тают. Какой из этого вывод? Мы должны написать код, устойчивый ко всем этим факторам. Как мы уже говорили, состояние приложения при обновлении становится довольно сложно предсказать, когда приложение обновляется без какого-либо механизма отслеживания состояния. Для того чтобы продолжить наш пример со списком, да­ вайте рассмотрим некоторый JavaScript-кoд для добавления нового элемента в список: (functi.on 111уАрр() { var li.stlteris = ["I love", "React", "and", "TypeScri.pt"]; var parentli.st = docu111ent.getEle111entByid("li.st-parent"); var addFor111 = docu111ent.getEle111entByid("add-i.te111-for111"); var newLi.stite111LaЬel = docu111ent.getEle111entByid("new-li.st-i.te111-laЬel"); = functi.on (event) { event.preventDefault(); addFor111.onsuЬP1i.t li.stite111s.push(newli.stite111LaЬel.value); renderli.stlteris(); }; functi.on renderli.stlte111s() { for (i. = 0; i. < li.stlte111s.length; i.++) { var el = docu111ent.createEle111ent("li.");
Глава 30 1 el.textContent = ltst!teмs[t]; parentLtst.appendChtld(el); } } renderLtstiteмs(); } )(); Этот фрагмент кода написан так, чтобы он был как можно более похож на ранние веб-приложения. Почему со временем все это перестает работать? В основном по­ тому, что приложения, предназначенные для масштабирования подобным образом, с течением времени начинают испытывать некоторые трудности в работе. Для этих приложений характерны следующие особенности. Подверженность ошибкам. Атрибут AddForм onsubмtt может быть легко переписан другими клиентскими JаvаSсriрt-программами, выполняющимися на странице. Вместо этого мы могли бы использовать • addEventLtstener, но это вызывает больше вопросов: Где и когда мы могли бы удалить этого слушателя с помощью reмoveEventltstener? • Не накопится ли у нас со временем много слушателей, если мы не будем ос­ торожны? • Что нам это будет "стоить"? • Как в это вписывается делегирование событий? Непредсказуемость. Наши источники достоверности неоднозначны: мы храним элементы списка в массиве JavaScript, но полагаемся на существующие элементы в DOM (напри­ td="ltst-parent") для завершения нашего при­ взаимозависимостей между JavaScript и HTML нам нужно мер, элемент с идентификатором ложения. Из-за этих рассмотреть еще несколько моментов: • Что делать, если по ошибке несколько элементов имеют одинаковый иденти­ фикатор? • Что делать, если элемент вообще не существует? • Что, если это не элемент списка ul? Можем ли мы добавлять элементы списка (lt-элементы) к другим родительским элементам? • Что произойдет, если вместо этого использовать имена классов? Наши источники достоверности смешаны между JavaScript и HTML, что де­ лает их ненадежными. Мы бы больше выиграли от наличия единого источни­ ка достоверности. Кроме того, элементы постоянно добавляются и удаляются из DOM на стороне клиента с помощью JavaScript. Если мы будем полагаться на наличие этих специфических элементов, надежная работа нашего прило-
Обзор для начинающих 31 жения не будет гарантирована, поскольку пользовательский интерфейс по­ стоянно обновляется. В данном случае наше приложение полно "побочных эффектов"; успех или неудача работы приложения зависит от проблем, воз­ никающих у пользователей. React исправил это, предложив модель, основан­ ную на функциональном программировании, в которой побочные эффекты намеренно помечаются и изолируются. Неэффективность. renderLi.stite111 отображает элементы на экране последовательно. Каждая моди­ DOM может потребовать больших вычислительных затрат, особенно фикация когда речь идет о смене макета и переформатировании. Поскольку мы находим­ ся на чужой планете с неизвестными вычислительными мощностями, это может быть весьма небезопасно для производительности в случае больших списков. Помните, мы планируем, что нашим крупномасштабным веб-приложением бу­ дут пользоваться миллионы пользователей по всему миру, в том числе те, у ко­ го есть устройства с низким энергопотреблением, но нет доступа к новейшим процессорам Apple МЗ Мах. Вероятно, более идеальным вариантом в этом сценарии, вместо последовательного обновления отдельного элемента списка DOM, было бы объединение этих операций и применение их всех к DOM од­ новременно. Но, возможно, как инженерам, нам не стоит этого делать, по­ скольку, возможно, браузеры в конечном итоге обновят технологию своей ра­ боты с помощью быстрых обновлений модели DOM и автоматически решат проблему за нас. Вот некоторые из проблем, которые преследовали веб-разработчиков в течение многих лет до появления React и других абстракций. Легко поддерживаемая, мно­ горазовая и предсказуемая упаковка кода при масштабировании была проблемой в отрасли с точки зрения стандартизации. В то время многие веб-компании испыты­ вали трудности, связанные с созданием надежных и масштабируемых пользова­ тельских интерфейсов. Именно в этот момент в Интернете появилось множество решений на основе JavaScript, направленных на решение этой проблемы: Backbone, AngularJS и jQuery. Давайте обсудим эти решения по очереди и по­ как они справлялись с проблемой. Это поможет нам понять, чем React КnockoutJS, смотрим, отличается от этих решений и, возможно, даже превосходит их. Библиотека jQuery Давайте рассмотрим, как мы ранее решали некоторые из этих проблем в Интернете, используя инструменты, предшествовавшие му React кнопкой важен. Мы начнем с jQuery React, и, таким образом, узнаем, поче­ и вернемся к нашему предыдущему примеру с Like. Напомним, что у нас есть кнопка Like интерактивной: <button i.d="li.keButton">Li.ke</button> в браузере, которую мы хотели бы сделать
32 Глава 1 С помощью jQuery мы бы добавили к ней поведение "like", $("#li.keButton").on("cli.ck", functi.on () { thi.s.prop("di.saЫed", true); fetch("/li.ke", { l'lethod: "POST", "Li.ke" }) , body: JSON.stri.ngi.fy({ li.ked: thi.s.text() }) . then( () => { "Li.ke" ? " Li.ked" "Li.ke" ) ; thi.s.text(thi.s.text() }) .catch( () => { thi.s.text("Fai.led"); }) .fi.nally(() => { thi.s.prop("di.saЫed", false); как мы делали ранее: }); }); Из этого примера мы видим, что мы привязываем данные к пользовательскому ии­ терфейсу и применяем эту привязку данных для обновления пользовательского ин­ терфейса на месте. jQuery как инструмент имеет достаточно возможностей в непо­ средственном управлении самим пользовательским интерфейсом. jQuery работает с большим "побочным эффектом", постоянно взаимодействуя и изменяя состояние вне своего собственного контроля. Мы говорим, что это "побоч­ ный эффект", потому что он позволяет вносить прямые и глобальные изменения в структуру страницы из любого места кода, в том числе из других импортированных модулей или даже из удаленно выполненного скрипта! Это может привести к не­ предсказуемому поведению и сложным взаимодействиям, которые трудно отсле­ дить и обосновать, поскольку изменения в одной части страницы могут повлиять на другие части непредвиденным образом. Эти разрозненные и неструктурированные манипуляции затрудняют поддержку и отладку кода. Современные фреймворки решают эти проблемы, предоставляя структурирован­ ные, предсказуемые способы обновления пользовательского интерфейса без пря­ мых манипуляций с DOM. В недавние же DOM, и ее было трудно времена была распространена технология воздействия на осмыслить и протестировать, поскольку мир вокруг кода, т. е. связанное с кодом состояние приложения, постоянно меняет­ ся. Однажды нам пришлось бы остановиться и спросить себя: "В каком же состоя­ нии сейчас находится приложение, открытое в браузере?" Это вопрос, ответить на который становилось все труднее по мере роста сложности наших приложений. Более того, эту кнопку с помощью всего лишь обработчик событий. jQuery сложно протестировать, потому что это
Обзор для начинающих 1 33 Если бы мы написали тест, код выглядел бы так: test("Li.keButton", О=> { const $button = $("#li.keButton"); expect($button.text()).toBe("Li.ke"); $button.tri.gger("cli.ck"); expect($button.text()).toBe("Li.ked"); }); Единственная проблема заключается в том, что ние null $( '#Li.keButton') возвращает значе­ в тестовой среде, потому что это ненастоящий браузер. Для тестирования этого кода нам пришлось бы имитировать среду браузера, а это большая работа. Это распространенная проблема с jQuery: код сложно протестировать, потому что трудно выделить добавляемое им поведение. браузера. Более того, jQuery jQuery также сильно зависит от среды разделяет право собственности на пользовательский интерфейс с браузером, что затрудняет анализ и тестирование: браузер владеет ин­ терфейсом, а всего лишь его гость. Это отклонение от парадигмы "одно­ jQuery- стороннего потока данных" было распространенной проблемой библиотек в то время. В конце концов, по мере развития Интернета фреймворк jQuery начал терять свою популярность, и стала очевидна потребность в более надежных и масштабируемых решениях. Хотя jQuery по-прежнему используется во многих производственных приложениях, он больше не является универсальным решением для создания со­ временных веб-приложений. Приведем некоторые причины, по которым jQuery впал в немилость. Вес и время загрузки. Одним из существенных недостатков полной библиотеки jQuery jQuery является его размер. Интеграция в веб-проекты увеличивает вес приложения, что мо­ жет быть особенно обременительным для веб-сайтов, нацеленных на быструю загрузку. В современную эпоху мобильных браузеров, когда у многих пользова­ телей может быть медленное соединение или ограниченное количество подклю­ чений для передачи данных, важен каждый килобайт. Таким образом, включе­ ние всей библиотеки jQuery может негативно сказаться на производительности и удобстве работы мобильных пользователей. React было предлагать конфигураторы для та­ Mootools, где пользователи могли выбирать желае­ Обычной практикой до появления ких библиотек, как jQuery и мую функциональность. Хотя это помогло уменьшить объем кода, усложнились решения, которые приходилось принимать разработчикам, а поэтому выросла трудоемкость процесса разработки в целом. Избыточность в современных браузерах. Когда jQuery только появился, он ликвидировал многие несоответствия между разными браузерами и предоставил разработчикам единый способ устранения этих различий в контексте выбора и последующего изменения элементов в брау-
Глава 34 1 зере. По мере развития Интернета развивались и веб-браузеры. Многие функ­ jQuery незаменимым инструментом, DOM или сетевая функциональность, ции, которые сделали тельное управление теперь данных, нием Использование jQuery поддерживаются всех во такие как последова­ связанная с извлече­ браузерах. современных для этих задач в современной веб-разработке представля­ ется избыточным, добавляющим ненужный уровень сложности. docuмent.queгySelector, например, довольно легко заменяет встроенный в API селектор jQuery $. Соображения, связанные с производительностью. Хотя jQuery упрощает многие задачи, это часто происходит за счет снижения производительности. Собственные методы JavaScript на уровне среды выполне­ ния улучшаются с каждой итерацией браузера и, таким образом, в какой-то мо­ мент могут выполняться быстрее, чем их аналоги в jQuery. Для небольших про­ ектов эта разница может быть незначительной. Однако в более крупных веб­ приложениях эти сложности могут накапливаться, что приводит к заметным рывкам или снижению быстродействия. По этим причинам, в то время как jQuery сыграл ключевую роль в развитии Интер­ нета и ответил на множество вызовов, с которыми сталкиваются разработчики, со­ временный веб-ландшафт предлагает нативные решения, которые часто делают менее актуальным. Как разработчики, мы должны взвесить удобство jQuery jQuery и его потенциальные недостатки, особенно в контексте текущих веб-проектов. В свое время jQuery, несмотря на присущие ему недостатки, произвел абсолютную революцию в том, как мы взаимодействовали с ственно настолько, что появились другие DOM. Влияние библиотеки, jQuery которые было суще­ использовали но добавили больше предсказуемости и возможности повторного использо­ вания кода. Одной из таких библиотек стала Backbone, которая была более ранней jQuery, попыткой решить те же проблемы, которые React решает сегодня. Давайте углу­ бимся в суть. Библиотека Библиотека Backbone Backbone, разработанная в начале 2010-х годов, стала одним из первых решений проблем (диссонанс между браузером и JavaScript, повторное использова­ ние кода, тестирование и т. д.), которые мы исследовали, прежде чем появился React. Это было элегантное и простое решение: библиотека, которая позволяла соз­ Backbone по-своему использовала традицион­ ный шаблон "Модель - Представление - Контроллер" 3 (рис. 1.1 ). Давайте немно­ го разберемся в этом шаблоне, который поможет нам понять React и заложит давать "модели" и "представления". основу для более качественного обсуждения. 3 Model-View-Controller (MVC, "Модель - Представление - Контроллер")- схема разделения дан­ ных приложения и управляющей логики на три отдельных компонента: модель, представление и контроллер симо. - - таким образом, что модификация каждого компонента может осуществляться незави­ Прим. пер.
Обзор для начинающих . . - - - - 1 Обновления 1 35 ~---. Контромер Модель ~ведомляет) ________ ., 1 Представление Рис. Шаблон Шаблон 1.1. Традиционный шаблон МVС MVC MVC - это философия проектирования, которая делит программные при­ ложения на три взаимосвязанных компонента, чтобы отделить внутреннее пред­ ставление информации от того, как эта информация предоставляется пользователю или принимается от него. Вот эти компоненты. Модель (Model). Модель отвечает за данные и бизнес-правила приложения. Модель не знает о представлении и контроллере, гарантируя, что бизнес-логика изолирована от пользовательского интерфейса. Представление (View). Представление - это пользовательский интерфейс приложения. Оно отобража­ ет данные из модели для пользователя и отправляет пользовательские команды контроллеру. Представление является пассивным, т. е. оно ожидает, пока модель предоставит данные для отображения, и не извлекает и не сохраняет данные на­ прямую. Представление также не обрабатывает взаимодействие с пользователем само по себе, а делегирует эту ответственность следующему компоненту - кон­ троллеру. Контроллер (Controller). Контроллер действует как интерфейс между моделью и представлением. Он принимает вводимые пользователем данные из представления, обрабатывает их (с возможными обновлениями модели) и возвращает выходные данные в пред­ ставление. Контроллер отделяет модель от представления, делая архитектуру системы более гибкой. Основным преимуществом шаблона MVC является разделение задач, что означает, что бизнес-логика, пользовательский интерфейс и вводимые пользователем данные разделены на различные разделы кодовой базы. Это не только делает приложение
Глава 36 1 более модульным, но и упрощает его обслуживание, масштабирование и тестиро­ вание. Шаблон MVC широко используется в веб-приложениях, и многие фрейм­ Django, Ruby on Rails и ASP.NET MVC, предлагают встроенную ворки, такие как поддержку для него. Шаблон MVC уже много лет является основным в разработке программного обес­ печения, особенно в веб-разработке. Однако по мере развития веб-приложений и роста ожиданий пользователей от интерактивных и динамических интерфейсов MVC. Вот где MVC React может решить эти проблемы. стали очевидны некоторые ограничения традиционного потерпеть неудачу, в то время как может Сложная интерактивность и управление состоянием. Традиционные архитектуры MVC часто сталкиваются с трудностями, когда дело доходит до управления сложными пользовательскими интерфейсами со множе­ ством интерактивных элементов. По мере развития приложения управление со­ стоянием и его влиянием на различные части пользовательского интерфейса может становиться громоздким. Это связано с увеличением количества контрол­ леров, которые иногда вступают в конфликт друг с другом, поскольку некото­ рые контроллеры могут управлять представлениями, которые к ним не относят­ ся. Это происходит, когда разделение между компонентами MVC не точно определено в коде продукта. с его компонентной архитектурой и виртуальным React, DOM, упрощает реше­ ния об изменениях состояюJя и их влиянии на пользовательский интерфейс, по сути, полагая, что компоненты пользовательского интерфейса подобны функ­ ции: они получают входные данные (elements) стила шаблон JavaScript (props) и возвращают выходные данные на основе входных данных. Эта ментальная модель радикально упро­ MVC, поскольку функции довольно широко распространены в и гораздо более доступны по сравнению с внешней ментальной моде­ лью, которая не является родной для языка программирования, похожего на МVС. Двусторонняя привязка данных. В некоторых МVС-фреймворках используется двусторонняя привязка данных, которая при неправильном управлении может привести к непредвиденным по­ бочным эффектам, когда в некоторых случаях либо представление не синхрони­ зируется с моделью, либо наоборот. Более того, при двусторонней привязке данных на вопрос о владении данными часто давался необработанный ответ с невнятным разделением интересов. Это особенно любопытно, поскольку, хотя MVC - это модель, проверенная многими командами разработчиков, которые полностью понимают, как правильно разделять проблемы в своих сценариях ис­ пользования, все же эти правила разделения редко применяются в жизни. Это особенно характерно в случае высокой скорости выпуска продукции и быстрого роста стартапов. Таким образом, разделение задач, одна из самых сильных сто­ рон МVС, часто превращается в слабость из-за отсутствия надлежащего приме­ нения. React использует счетчик шаблонов для двусторонней привязки данных, назы­ ваемый "однонаправленным потоком данных" (подробнее об этом позже), для
Обзор для начинающих 1 37 того чтобы сделать приоритетным и даже усилить однонаправленный поток данных в таких системах, как Forget (которые мы также обсудим далее в книге). Такие подходы делают обновления пользовательского интерфейса более пред­ сказуемыми, позволяют нам более четко разделять задачи и в конечном счете способствуют быстрому росту команд разработчиков программного обеспечения. Тесная связь. В некоторых реализациях МVС-модели модель, представление и контроллер мо­ гут быть тесно связаны, что затрудняет изменение, или рефакторинг, одного из них без ущерба для других. React поощряет более модульный и несвязанный подход с помощью своей модели, основанной на компонентах, позволяя и под­ держивая совместное размещение зависимостей по близости от их представле­ ний в пользовательском интерфейсе. Нам не нужно слишком вдаваться в подробности этого шаблона, поскольку это книга по React, но для наших целей нужно подчеркнуть, что модели концептуально были источниками данных, а представления - концептуальными пользователь­ скими интерфейсами, которые использовали и отображали данные. портировала удобные API Backbone экс­ для работы с этими моделями и представлениями и пре­ доставила способ соединения моделей и представлений. Это решение было очень мощным и гибким для своего времени. Кроме того, это бьmо масштабируемое ре­ шение, которое позволяло разработчикам тестировать свой код изолированно. Ниже представлен предыдущий пример кнопки, но на этот раз с использованием Backbone: const Li.keButton = BackЬone.Vi.ew.extend({ tagNaмe: "button", attri.butes: { type: "button", }, events: { cHck: "onCHck", }, i.ntti.aHze() { thi.s.l'Юdel.on("change", thi.s.render, thi.s); }, render() { thi.s.$el.text(thi.s.мodel.get("Hked")? гetuгn "Li.ked" "Li.ke"); thi.s; }, onCHck() { fetch("/Hke", { мethod: "POST", Ьоdу: JSON. stri.ngi.fy( { Hked: !thi.s.J110del .get( "Hked") }) , })
Глава 38 1 . then( О => { thi.s.f'Юdel.set("l'i.ked", !thi.s.PIOdel.get("l'i.ked")); }) .catch( О => { thi.s.flIOdel.set("faHed", true); }) . fi.nally( О => { thi.s.f'Юdel.set("pend'i.ng", false); }); }, }); const l'i.keButton = new L'i.keButton({ f'Юdel: new Backbone.Мodel({ l'i.ked: false, }), }); docu111ent.Ьody.appendCh'i.ld(l'i.keButton.rendeг().el); Вы заметили, что кнопка L'i.keButton расширяет рендеринга, который возвращает значение th'i.s? Backbone.V'i.ew и что у нее есть метод Далее мы рассмотрим аналогичный метод рендеринга в но давайте не будем забегать вперед. Здесь также стоит отметить, что в не было реальной реализации для рендеринга. Вместо React, Backbone этого вы либо вручную изменяли тему шаблонов, такую как Библиотека Backbone DOM Handlebars. с помощью предоставила цепочечный jQuery, API, либо использовали сис­ который позволил разработ­ чикам размещать логику в виде свойств объектов. Сравнивая это с нашим преды­ дущим примером, мы видим, что Backbone значительно упростила создание инте­ рактивной кнопки, которая обновляет пользовательский интерфейс в ответ на события. Она также делает это более структурированным способом, используя группировку логики. Обратите внимание, что Backbone потому что мы можем создать экземпляр упростила тестирование этой кнопки, l 'i.keButton и затем вызвать его метод геndег для тестирования. Мы тестируем этот компонент следующим образом: test("L'i.keButton 'i.n'i.t'i.al state", О=> { const l'i.keButton = new L'i.keButton({ f'Юdel: new BackЬone.Model({ l'i.ked: false, // Начальное сос~ояние }), }); ус~ановлено в not liked
Обзор для начинающих ltkeButton.гender(); // // 1 39 Вызов рендеринга для Оf'lражения начального сосf'/Ояния "L ike ", Проверка наличия f'leKCl'IO Оf'lражак,цего начальное сосf'/Ояние expect(HkeButton.el.textContent).toВe("Ltke"); }); Мы даже можем протестировать поведение кнопки после изменения ее состояния, в случае события cHck, например, так: test("LtkeButton", async О=> { // Помеf'/UМ функцию как async, Чf'/обы const ltkeButton = new LtkeButton({ обробаrюf'lь prOl'lise new BackЬone.Мodel({ Hked: false, l'Юdel: }), }); expect(HkeButton.гender().el.textContent).toВe("Ltke"); // Им~lf'lируем fetch, чf'/обы global.fetch = jest.fn(() избежаf'lь реального НТТР-запроса => PrOl'1tse.гesolve({ json: () ltked: => Proмtse.гesolve({ tгue }), }) ); // onClick для awatt ltkeButton.onCltck(); Задержим меf'/Од обеспечения завеf)IIJения асинхронных операций expect(HkeButton.гendeг().el.textContent).toBe("Ltked"); // Опционально воссf'/йновим fetch до первоначальной реализации global.fetch.мockRestoгe(); }); В то время библиотека Backbone предоставляла определенные удобства и была очень популярным решением. Альтернативой было написание большого количест­ ва кода, который было трудно тестировать и с которым трудно было разобраться, и не было гарантий того, что код будет работать должным образом и надежно. Таким образом, Backbone была очень желанным решением. На заре своего существования она приобрела популярность благодаря простоте и гибкости, что, как правило, не обходится без критических замечаний. Вот некоторые из негативных моментов, связанных с Backbone.js.
Глава 40 1 Многословный- и шаблонный код. Одним из частых замечаний к Backbone.js является то, что разработчикам при­ ходится писать слишком много шаблонного кода. Для простых приложений это может оказаться несложным, но по мере роста приложения увеличивается и объем шаблонного кода, что приводит к потенциально избыточному и сложному в обслуживании коду. Отсутствие двусторонней привязки данных. В отличие от некоторых своих современников, Backbone.js не поддерживает встроенную двустороннюю привязку данных. Это означает, что при изменении данных DOM не обновляется автоматически, так же как и изменения модели не означают автоматического обновления данных. Разработчикам часто приходит­ ся писать пользовательский код или использовать плагины для реализации этой функциональности. Архитектура, управляемая событиями. Обновление данных модели может вызвать множество событий во всем прило­ жении. Этот каскад событий может стать неуправляемым, что приведет к ситуа­ ции, когда неясно, как изменение одного фрагмента данных повлияет на осталь­ ную часть приложения, а это затрудняет отладку и обслуживание. Для того чтобы решить эти проблемы, разработчикам часто приходилось более тщатель­ но подходить к управлению событиями, чтобы при обновлении предотвратить "волновой эффект" во всем приложении. Отсутствие возможности компоновки. В Backbone.js отсутствуют встроенные функции для простого иерархического вложения представлений, что может затруднить создание сложных пользова­ тельских интерфейсов. React, напротив, обеспечивает плавное вложение компо­ нентов с помощью дочернего свойства (prop ), что значительно упрощает созда­ ние сложных иерархий пользовательского интерфейса. Backbone, Marionette.js, расширение позволяет решить некоторые из этих проблем с компоновкой, но оно все же не обеспечивает такого комплексного решения, как компонентная модель React. Наличие у библиотеки Backbone.js проблем еще раз доказывает, что ни один инст­ румент или фреймворк не идеален. Лучший выбор часто зависит от конкретных потребностей проекта и предпочтений команды разработчиков. Также стоит отме­ тить, что успех инструментов веб-разработки во многом зависит от сильного сооб­ щества, и, к сожалению, у Backbone.js в последние годы наблюдался спад популяр­ React. Некоторые могут сказать, что React убил ее, ности, особенно с появлением но мы пока воздержимся от таких суждений. Библиотека KnockoutJS Давайте рассмотрим другое популярное в свое время решение - КnockoutJS. КnockoutJS, разработанная в начале 2010-х годов, представляла собой библиотеку,
Обзор для начинающих 1 41 которая позволяла создавать "наблюдаемые объекты" (observaЫes) и "привязки" (bindings ), используя отслеживание зависимостей при изменении состояния. КnockoutJS была одной JavaScript, из первых, если не первой, реактивной библиотекой где реактивность определяется как обновление значений в ответ на из­ менения состояния наблюдаемого объекта. Современные подходы к этому стилю реактивности иногда называются "сигналами", и они широко распространены в та­ ких библиотеках, как Vue.js, Solids, Svelte, Qwik, modem Angular 10. и др . Более под­ робно мы рассмотрим их в главе Концептуально наблюдаемые объекты были источниками данных, а привязки - пользовательскими интерфейсами, которые использовали и отображали эти дан­ ные: наблюдаемые объекты были похожи на модели, а привязки были похожи на представления. Однако в качестве некоторой эволюции шаблона MVC, который мы обсуждали ра­ нее, КnockoutJS вместо этого больше работала по шаблону или в стиле MVVМ (рис. 1.2). Давайте разберемся Model-View-ViewModel с этой структурой более подробно. Моdе1 ViewModel Привязка данных Презентация и логика презентации Бизнес-логика и данные Рис.1.2. Шаблон МVVM Шаблон MVVM Шаблон MVVM - это архитектурный шаблон проектирования, который особенно популярен в приложениях с богатым пользовательским интерфейсом, например, это созданных с использованием таких платформ, как WPF и Xamarin. MVVМ эволюция традиционного шаблона Model-View-Controller (МVС), адаптированного для современных платформ разработки пользовательского интерфейса, где важной особенностью является привязка данных . Ниже приведена разбивка компонентов MVVM. Модель (Model) . • Представляет данные и бизнес-логику приложения . • Отвечает за извлечение, хранение и обработку данных. • Обычно взаимодействует с базами данных, службами, другими источниками данных и операциями . • Ничего не знает о Представление View и ViewModel. (View). • Представляет пользовательский интерфейс приложения. • Отображает информацию для пользователя и принимает вводимые пользова­ телем данные.
Глава 42 • 1 В шаблоне MVVM View является пассивным компонентом и не содержит никакой логики приложения. Вместо этого он декларативно привязывается к ViewModel, автоматически отражая изменения с помощью механизмов при­ вязки данных. Представление-Модель • • (ViewModel). Служит связующим звеном между Model Предоставляет данные и команды для и View. привязки View. Данные здесь часто на­ ходятся в формате, готовом к отображению. • • Обрабатывает пользовательский ввод, часто с помощью командных шаблонов. Содержит логику презентации и преобразует данные из торый может быть легко отображен во • Примечательно, что ViewModel Model в формат, ко­ View. не знает о конкретном View, который его ис­ пользует, что позволяет пользоваться преимуществами независимой архитек­ туры. Ключевым преимуществом шаблона ное MVC, MVVM является разделение задач, аналогич­ что предоставляет нам следующие возможности. Возможность тестирования. Разделение ViewModel и View упрощает написание модульных тестов для логи­ ки презентации без использования пользовательского интерфейса. Возможность повторного использования. ViewModel можно повторно использовать в разных представлениях или плат­ формах. Удобство сопровождения. Четкое разделение упрощает управление кодом, его расширение и рефакторинг. Возможность привязки данных. Шаблон отлично подходит для платформ, поддерживающих привязку данных, поскольку сокращает объем шаблонного кода, необходимого для обновления пользовательского интерфейса. Поскольку мы обсуждаем шаблоны MVC чтобы понять различия между ними (табл. и MVVM, 1.1 ). Таблица 1.1. давайте быстро сравним их, Сравнение шаблонов MVC и MVVM Критерий MVC Основная цель В первую очередь для веб- Специально разработан для прило- приложений, отделяющих пользо- жений с богатым пользовательским вательский интерфейс от логики MVVM интерфейсом, особенно с двусторонней привязкой данных (на- стольные компьютеры или SP А4) 4 SPA (sin~le pa~ed application)- веб-приложение, которое взаимодействует с пользователем путем - Прим. пер. динамического переписывания текущей страницы.
43 Обзор для начинающих Таблица Критерий MVC Компоненты Model: 1.1 (окончание) MVVM данные и бизнес-логика. пользовательский View: View: данные и бизнес-логика. элементы пользовательского интерфейса. интерфейс. Contro\ler: Model: управляет пользова- тельским вводом, обновляет пред- ViewModel: View мост между Model и ставление Поток данных Controller управляет пользователь- ским вводом, который обновляет Model, а затем и View View напрямую привязывается к ViewMode\. Изменения во View автоматически отражаются во ViewModel Независимость View и Controller часто тесно свя- заны и наоборот Высокая степень развязки, по- скольку ViewModel ничего не знает View, использующем о конкретном его Итерации поль- Управляются при помощи Обрабатываются с помощью при- зователя Controller вязки данных и команд во ViewModel Пригодность Широко используется при разра- Подходит для платформ, поддер- платформы ботке веб-приложений (например, живающих надежную привязку Ruby on Rails, Django, ASP.NET MVC) данных (например, WPF, Xamarin) Из этого краткого сравнения мы можем видеть, что реальная разница между шаб­ MVC и MVVM заключается в связывании: при отсутствии Controller между Model и View владение данными становится понятнее и ближе к пользователю. React еще больше улучшает MVVM благодаря однонаправленному потоку данных, лонами который мы обсудим чуть позже. Таким образом, еще больше сужается область владения данными, так что состояния принадлежат конкретным компонентам, ко­ торые в них нуждаются. А пока давайте вернемся к КnockoutJS и к тому, как он со­ относится с React. КnockoutJS экспортировал АРI-интерфейсы для работы с этими наблюдаемыми объектами и привязками. Давайте посмотрим, как бы мы реализовали кнопку в KnockoutJS. Это поможет нам понять, почему кнопки на КnockoutJS: functton createVtewМodel({ ltked }) { const tsPendtng = ko.observaЫe(false); const hasFatled = ko.observaЫe(false); const onCltck = () => { tsPendtng(tгue); React Like лучше. Вот версия нашей
Глава 44 1 fetch("/li.ke", { Piethod: "POST", Ьоdу: JSON.strtngtfy({ ltked: !ltked() }), }) . then( О => { li.ked( !l tked()); }) .catch(() => { hasFatled(tгue); }) .fl.natly(() => { tsPendtng(false); }); }; { tsPendtng, hasFaHed, onCltck, li.ked, гetuгn }; } ko.applyBtndtngs(createVtewМodel({ ltked: В КnockoutJS "модель представления" ko.observaЫe(false) - это объект })); JavaScript, содержащий ключи и значения, которые мы привязываем к различным элементам нашей страницы с помощью атрибута data-blnd. В КnockoutJS нет "компонентов" или "шаблонов", только модель представления и способ привязки ее к элементу в браузере. Наша функция createVtewМode l - это то, как мы создаем модель представления с помощью Кnockout. Затем мы используем ko.applyBtndtngs для подключения модели ko.applyBtndtngs использует браузере все элементы, .имеющие атрибут data- представления к среде хостинга (браузеру). Функция модель представления и находит в blnd, который Кnockout использует для привязки элементов к модели представ­ ления. Кнопка в нашем браузере бьmа бы привязана к свойствам этой модели представле­ ния следующим образом: <Ьutton data-blnd="cli.ck: onCli.ck, text: li.ked? 'Ltked' : tsPendtng? [ ... ] ></Ьutton> Обратите внимание, что этот код бьm сокращен для простоты восприятия. Мы привязываем НТМL-элемент к "модели представления", которую мы создали с помощью нашей функции createVtewМodel, и сайт становится интерактивным. Как
Обзор для начинающих 1 45 вы можете себе представить, явная "подписка" на изменения в наблюдаемых объек­ тах и последующее обновление пользовательского интерфейса в ответ на эти изме­ нения это большая работа. КnockoutJS была отличной библиотекой для своего - времени, но для ее применения также требовалось много шаблонного кода. Более того, модели представления часто становились очень большими и сложными, что приводило к росту неопределенности в отношении рефакторинга и оптимиза­ ции кода. В итоге мы получили подробные монолитные модели представлений, ко­ торые было сложно тестировать и анализировать. Тем не менее КnockoutJS бьmа очень популярным решением и отличной библиотекой для своего времени. Ее объ­ екты также было относительно легко тестировать изолированно, что было большим достоинством. Вот как мы будем тестировать нашу кнопку в КnockoutJS: test("Li.keButton", const vi.ewМodel О=> { = createVi.ewМodel({ li.ked: ko.observaЫe(fatse) }); expect(vi.ewМodel.li.ked()).toBe(fatse); vi.ewМodel.onCli.ck(); expect(vi.ewМodel.li.ked()).toВe(tгue); }); Библиотека Библиотека AngularJS AngularJS была разработана компанией новаторский фреймворк на JavaScript, Google в 2010 году. Это бьm оказавший значительное влияние на индуст­ рию веб-разработки. Данная библиотека резко контрастировала с библи()теками и фреймворками, которые мы обсуждали выше, поскольку включала в себя несколь­ ко инновационных функций, влияние которых можно увидеть в последующих вер­ сиях библиотек, включая React. Далее мы сравним AngularJS с другими библиоте­ ками и рассмотрим ее ключевые функции, а также попытаемся понять, какой путь она проложила для React. Двусторонняя привязка данных Отличительной особенностью AngularJS была двусторонняя привязка данных, ко­ торая значительно упростила взаимодействие между пользовательским интерфей­ сом и исходными данными. Если модель (данные) изменяется, представление (пользовательский интерфейс) автоматически обновляется, чтобы отразить это из­ менение, и наоборот. Это резко контрастировало с такими библиотеками, как jQuery, где разработчикам приходилось вручную манипулировать DOM, чтобы от­ ражать любые изменения в данных и фиксировать вводимую пользователем ин­ формацию для обновления данных. Давайте рассмотрим простое приложение вязка данных играет решающую роль: <!DОСТУРЕ <ht::rll> ht111l> AngularJS, в котором двусторонняя при­
Глава 46 1 <head> <scг\.pt src="https://ajax.googleapi.s.c0fl1/ajax/li.bs/angularjs/1.8.2/angular.f'lin.js"> </scri.pt> </head> ng-app=""> <p>Naf'le: <i.nput type="text" ng-l'Юdel="naf'le" <р ng-i.f="naf'le">Нello, {{naf'le}}!</p> <Ьоdу /></р> </Ьоdу> </hvit> В этом приложении директива ng-l'Юdel привязывает значение поля ввода к имени переменной. По мере ввода название модели обновляется, а представление данном случае приветствие "Не Но, {{naf'le}} ! " - - в обновляется в режиме реального времени. Модульная архитектура Библиотека AngularJS представила модульную архитектуру, которая позволила разработчикам логически разделять компоненты своих приложений. Каждый мо­ дуль может включать в себя нужную функциональность, а также разрабатываться, тестироваться и поддерживаться независимо. Некоторые назвали бы это предшест­ венником компонентной модели React, но это спорный вопрос. Вот краткий пример: = angular.l'Юdule("l'lyдpp", [ "ngRoute", "appRoutes", "userCtrl", "userServ'\.ce", var арр ]); v1r userCtrl = angular.l'Юdule("userCtrl", []); userCtrl.controller("UserController", functi.on ($scope) { $scope,f'lessage = "Нello fr0fl1 UserController"; }); v1r userServ'\.ce = angular.l'Юdule("userServ'\.ce", []); userServ'\.ce.factory("User", functi.on ($http) { // ... }); В предыдущем примере модуль ngRoute, appRoutes, userCtrl и f'lyApp зависит от нескольких других модулей: userServ'\.ce. Каждый зависимый модуль может нахо-
Обзор для начинающих диться в отдельном файле дуля l'lyApp. 1 47 и разрабатываться отдельно от основного мо­ JavaScript Эта концепция существенно отличалась от jQuery и Backbone.js, в кото­ рых не было понятия "модуль" в этом смысле. Мы внедряем эти зависимости (appRoutes, userCtrl и т. д.) в наше корневое прило­ жение, используя шаблон под названием внедрение зависимостей injection), который был популяризирован в Angular. (dependency Нет необходимости говорить, что этот шаблон был распространен до стандартизации модулей JavaScript. С тех пор операторы i.l'lpoгt и ехрогt быстро взяли верх. Для сравнения этих зависимостей с компонентами React поговорим о внедрении зависимостей немного подробнее. Внедрение зависимостей Внедрение зависимостей (dependency injection, DI) - это паттерн проектирования, в котором объект получает свои зависимости, а не создает их. AngularJS включил этот паттерн проектирования в свою основу, что в то время не было распростране­ но в других библиотеках JavaScript. Это оказало глубокое влияние на методы соз­ дания модулей и компонентов и на технику управления ими, способствуя повыше­ нию степени модульности и возможности повторного использования. Вот пример того, как работает vаг арр DI в AngularJS: = angular.l'lodule("l'lyApp", []); app.controlleг("l'lyController", $scope.greeti.ng functi.on ($scope, l'lyServi.ce) { = l'lyServi.ce.sayHello(); }); functi.on return { sayHello: functton () { return "Hello, World!"; app.factoгy("l'lyServi.ce", О { }, }; }); В приведенном примере l'lyContгoller через DI. l'lyServ'lce - это сервис, который внедряется в контроллер Контроллеру не нужно знать, как создать сервис. Он просто объявляет сервис как зависимость, а AngularJS заботится о ее создании и внедре­ нии. Это упрощает управление зависимостями, повышает тестируемость и возмож­ ность повторного использования компонентов. Сравнение с Backbone.js и Backbone.js и Knockout.js Knockout.js были двумя популярными библиотеками, которые уже использовались в то время, когда был представлен Angulars. У обеих библиотек
Глава 48 1 бьmи свои сильные стороны, но им не хватало некоторых функций, встроенных в AngularJS. Backbone.js, например, давал разработчикам больше контроля над своим кодом и был менее самоуверенным по сравнению с AngularJS. Такая гибкость была одно­ временно и сильной, и слабой стороной: она позволяла применять больше настро­ ек, но также AngularJS требовала большего количества с ее двусторонней привязкой данных и шаблонного DI кода. Библиотека позволяла создавать большую структурируемость. В ней было больше самоуверенности, что привело к увеличе­ нию скорости разработки: то, что мы видим в современных фреймворках Remix и т. д. Это одна из причин, почему AngularJS Next.js, намного опередила свое время. У Backbone также не было решения по прямому изменению представления (модели DOM), и библиотека часто оставляла это на усмотрение разработчиков. AngularJS позаботилась о мутациях DOM с помощью двусторонней привязки данных, что бы­ ло большим достоинством. Кnockout.js в основном была ориентирована на привязку данных, и в ней отсутст­ вовали кое-какие мощные инструменты, которые предоставляет мер DI и модульная архитектура. AngularJS, AngularJS, напри­ являясь полноценным фреймворком, предлагала более комплексное решение для создания одностраничных приложений (SPA). Хотя обновления под названием Angular AngularJS прекратились, сегодня версия этой библиотеки предлагает тот же, хотя и расширенный набор всеобъемлю­ щих преимуществ, которые делают ее идеальным выбором для крупномасштабных приложений. Компромиссы AngularJS С момента своего появления AngularJS (1.х) представляла собой значительный ры­ вок в практике веб-разработки. Однако, поскольку сфера веб-разработки продолжа­ ла стремительно развиваться, некоторые аспекты AngularJS рассматривались как ограничения или слабые стороны, которые со временем привели к ее относитель­ ному упадку. Перечислим эти ограничения. Производительность. У AngularJS бьmи проблемы с производительностью, особенно в крупномас­ штабных приложениях со сложными привязками данных. Цикл "переваривания" (digest cycle) в AngularJS, основная функция для обнаружения изменений, может привести к медленному обновлению и запаздыванию пользовательских интер­ фейсов в больших приложениях. Двусторонняя привязка данных, будучи инно­ вационной и полезной во многих ситуациях, также способствовала снижению производительности. Сложность. AngularJS представила ряд новых концепций, включая директивы, контроллеры, сервисы, внедрение зависимостей, фабрики и многое другое. Хотя эти функции сделали AngularJS мощным средством, они также усложнили ее и сделали труд­ ной для изучения, особенно для начинающих. Например, часто обсуждаемый
Обзор для начинающих 49 вопрос "Что это должно быть: фабрика или сервис?" ставил в тупик некоторые команды разработчиков. Проблемы Jwuгpaцuu на Angular 2+. Когда была анонсирована версия вместимости с AngularJS Angular 2, она не предполагала обратной со­ 1..х и требовала написания кода на Dart и/или TypeScript. Это означало, что разработчикам пришлось переписать значительную часть сво­ Angular 2, что было воспринято как серьезное пре­ Angular 2+ существенно раскололо сообщество Angular и проложив путь для React. его кода для обновления до пятствие. Появление вызвало путаницу, Сложный синтаксис шаблонов. допускает сложные выражения AngularJS пример JavaScript в атрибутах шаблона, на­ on-cli.ck="$ctrl.sol'1e.deeply.nested. fi.eld = 123". Это стало проблематич­ ным, поскольку приводило к смешению представления и бизнес-логики в рамках разметки (markup). Такой подход создавал проблемы с поддержкой, поскольку расшифровка и управление таким "переплетенным" кодом становились гро­ моздкими. Кроме того, отладка была более сложной, поскольку слои шаблонов по своей сути не были предназначены для обработки сложной логики, а любые ошибки, возникавшие по этой причине, могли быть сложными для поиска и устранения. Вдобавок такая практика нарушает принцип разделения задач, который является фундаментальной философией проектирования, предусматривающей раздель­ ную обработку различных аспектов приложения для улучшения качества кода и удобства сопровождения. Теоретически, шаблон должен только вызывать метод контроллера для выпол­ нения обновления, однако сложность шаблона ничем не ограничивалась. Отсутствие безопасности типов. Шаблоны в AngularJS не TypeScript, такими как в работали со статическими средствами проверки типов, что затрудняло выявление ошибок на ранних стадиях процесса разработки. Это было существенным недостатком, особенно для круп­ номасштабных приложений, где безопасность типов имеет решающее значение для удобства обслуживания и масштабирования. Запутанная модель Объект $scope. $scope в AngularJS часто оказывался источником путаницы из-за его роли в привязке данных и его поведения в различных контекстах, поскольку он слу­ жил связующим звеном между представлением и контроллером, но его поведе­ ние не всегда было интуитивным или предсказуемым. Это привело к сложностям, особенно для новичков, в понимании того, как син­ хронизируются данные между моделью и представлением. Кроме того, может наследовать свойства родительского $scope $scope во вложенных контроллерах, что затрудняет отслеживание точки, где изначально было определено или изме­ нено конкретное свойство $scope.
Глава 50 1 Такое наследование может вызвать неожиданные побочные эффекты в прило­ жении, в частности при работе с вложенными областями, где родительская и до­ черняя хии $scope $scope и могут непреднамеренно влиять друг на друга. Концепция иерар­ прототипное наследование, на котором эта концепция была основана, часто расходились с более традиционными и знакомыми правилами определения лексической области видимости, используемыми в JavaScript. Это добавляло еще один уровень сложности в обучении. React, например, группирует состояние с компонентом, который в нем нуждает­ ся. Такой подход позволяет полностью избежать этой проблемы. Ограниченные средства разработки. не предлагала большого набора инструментов разработчика для от­ AngularJS ладки и профилирования производительности, особенно если сравнивать с инст­ рументами DevTools, доступными в возможности для отладки приложений Введение в широкие React Примерно в это же время предложенных React, которые предоставляют React в процессе разработки. React, React приобрел известность. Одной из основных идей, стала основанная на компонентах архитектура. Хотя реализа­ ция отличается, основная идея осталась прежней - создавать оптимальные пользо­ вательские интерфейсы для Интернета и других платформ с помощью повторно используемых компонентов. В то время как библиотека AngularJS ставлений к моделям, представил React использовала директивы для привязки пред­ JSX и радикально упрощенную компонент­ ную модель. Тем не менее кто-то может возразить, что не имей опыта, заложенного к модели В React AngularJS однако у AngularJS React за плечами в продвижении компонентной архитектуры, переход мог бы пройти не так гладко. модель двусторонней привязки данных была отраслевым стандартом; нее также имелись и некоторые недостатки, такие проблемы с производительностью в больших приложениях. как потенциальные React извлек из этого факта урок и внедрил схему однонаправленного потока данных, которая дает раз­ работчикам больше контроля над своими приложениями и облегчает понимание того, как данные меняются с течением времени. React также представил виртуальный DOM, о котором вы прочтете в главе 3. Эта концепция повысила производительность за счет минимизации прямых манипуля­ ций с DOM. AngularJS, с другой стороны, часто напрямую манипулировала DOM, что могло привести к проблемам с производительностью и некоторым несогласо­ ванным состояниям, которые мы недавно обсуждали в разделе о Тем не менее AngularJS jQuery. внесла значительный вклад в практику, и было бы непра­ вильно не упомянуть, что эта библиотека не только произвела революцию в веб­ разработке, но и проложила путь для эволюции будущих фреймворков и библио­ тек, одной из которых является React.
Обзор для начинающих Давайте посмотрим, как React вписывается в реальный мир и откуда взялся 51 React на данном этапе истории. В то время обновление пользовательского интерфейса все еще было относительно сложной и не до конца решенной задачей. На сегодняшний день эта задача еще далека от окончательного решения, но благодаря React пробле­ мы стали заметно менее сложными и вдохновили разработчиков на создание дру­ гих библиотек, таких как Solids, Qwik и др. Facebook компании Meta 5 не стал ис­ ключением из-за сложности пользовательского интерфейса и масштабируемости. В результате Meta создала ряд внутренних решений, дополняющих уже существо­ вавшие на тот момент. Одним из первых был женеры Facebook BoltJS - инструмент, в который ин­ "собрали на болтах" функции, которые им показались интерес­ ными. Была собрана комбинация инструментов, нацеленных на то, чтобы сделать обновления веб-интерфейса пользователя Примерно в это же время инженеру Facebook интуитивно Уолку Facebook Джордану понятнее. (Jordan Walke) при­ шла в голову радикальная идея, которая изменила существовавший на тот момент статус-кво и полностью заменила прежние минимальные обновляемые части веб­ страниц новыми. Как мы уже видели ранее, библиотеки JavaScript управляли отно­ шениями между представлениями (пользовательскими интерфейсами) и моделями (источниками данных), используя парадигму, называемую двусторонней привязкой данных. В свете ограничений этой модели, которые мы обсуждали ранее, идея Джордана заключалась в том, чтобы вместо этого использовать идею, называемую односторонним потоком данных. Это была гораздо более простая парадигма, и она могла эффективнее синхронизировать представления и модели. Так родилась одно­ направленная архитектура, которая впоследствии легла в основу Ценное предложение, внесенное React. React Итак, урок истории окончен. Надеюсь, теперь у нас достаточно информации, чтобы начать понимать, почему React- это нечто особенное. Учитывая, как легко было попасть в ловушку небезопасного, непредсказуемого и неэффективного JavaScript-кoдa в больших масштабах, нам требовалось решение, которое неожиданно привело бы нас к успеху. Давайте поговорим о том, как именно React это делает. Сравнение декларативного и императивного кода React предоставляет декларативную абстракцию для DOM. Позже в книге мы пого­ ворим о том, как это делается, более подробно, но, по сути, это дает нам возмож­ ность писать код, который выражает то, что мы хотим видеть. Затем мы заботимся о том, как это реализовать, обеспечивая создание и работу нашего пользователь­ ского интерфейса в условиях безопасности, предсказуемости и эффективности. Давайте рассмотрим приложение list, которое мы создали ранее. В бы переписать его следующим образом: functi.on MyL'\.st() { const [i.teмs, setiteмs] = useState(["I tove"]); 5 Признана экстремистской и запрещена на территории РФ. - Прим. ред. React мы могли
Глава 52 гetuгn 1 ( <di.v> <Ul> {tte~s.~ap((t) => ( <li. key={t /* keep tte~s untque */}>{t}</li.> ))} </ul> <Newiter1Fom onAddite~{(newlte~) => setltel'1s([ ... ttel'1s, newltel'I])} /> </di.v> ); } Обратите внимание, что в return мы буквально пишем что-то похожее на HTML, чтобы результат выглядел так, как мы хотим его видеть. Я хочу видеть окно с Newlte~For~ и списком. Ой, а как он туда попадает? Это должен выяснить React. Бу­ дем ли мы группировать элементы списка, чтобы добавлять их все сразу? Или ста­ нем добавлять их последовательно, один за другим? React описывает, как это дела­ ется, в то время как мы просто сообщаем, что мы хотим сделать. В следующих главах мы углубимся в React и рассмотрим, как именно он это делает на момент написания этой книги. Должны ли мы тогда ссылаться на НТМL-элементы с помощью имен классов? Ис­ пользуем ли мы getEle~ntByid? Нет. React создает для нас уникальные "элементы React" ("React elements"), с помощью которых он обнаруживает изменения и посте­ пенно вносит обновления. Поэтому нам не нужно считывать имена классов и дру­ гие идентификаторы из пользовательского кода, существование которых мы не можем гарантировать: нашим источником достоверности становятся исключитель­ но JavaScript и React. Мы экспортируем наш компонент Myl tst в и React, React без лишних вопросов вы­ водит его на экран безопасным, предсказуемым и производительным способом. За­ дача компонента - просто вернуть описание того, как должна выглядеть эта часть пользовательского интерфейса. Он делает это с помощью виртуалыюго DOM, vDOM), структуры DOM пользовательского интерфейса. после обновления с виртуальным DOM Затем React соответствие с виртуальным Виртуальный DOM, DOM. сравнивает виртуальный до обновления и преобразует это в не­ большие производительные обновления для реального Виртуальный DOM (virtual который представляет собой упрощенное описание предполагаемой Таким образом DOM, чтобы привести его в React вносит изменения в DOM. DOM DOM - это программная концепция, которая представляет реальный но в виде объекта JavaScript. Если на данный момент такая формулировка слишком сложна, не волнуйтесь: этому подходу посвящена глава 3, в которой все описано более подробно. На данный момент важно знать, что виртуальный DOM позволяет разработчикам обновлять пользовательский интерфейс, не манипулируя
Обзор для начинающих непосредственно самим DOM. React использует виртуальный DOM 53 для отслежива­ ния изменений в компоненте и визуализирует компонент только при необходимо­ сти. Этот подход быстрее и эффективнее, чем обновление всего дерева DOM при каждом изменении. это упрощенное представление реального дерева В React виртуальный DOM DOM. Это простой объект JavaScript, который описывает структуру и свойства элементов пользовательского интерфейса. React создает и обновляет виртуальный DOM в соответствии с фактическим деревом DOM, и любые изменения, внесенные в виртуальный DOM, применяются к фактическому DOM с помощью процесса, на­ зываемого согласованuе/н (reconciliation). Этому посвящена глава 4, но для контекста нашего обсуждения давайте рассмот­ рим небольшое резюме с несколькими примерами. Для того чтобы понять, как ра­ ботает виртуальный компонент React, DOM, вернемся к нашему примеру с кнопкой который отображает кнопку Like Like. Мы создадим и количество лайков. Когда пользователь нажимает на кнопку, количество лайков должно увеличиться на единицу. Вот код для нашего компонента: \Jllport React, { useState} frOl'I "react"; functton LikeButton() { const [likes, setlikes] = useState(0); functton handlelike() { setlikes(likes + 1); } return ( <dtv> <button onClick={handleLike}>Like</button> <p>{likes} Likes</p> </dtv> ); } ехрогt default LikeButton; в этом коде мы использовали хук 6 Hkes, useState ~ для создания переменном состояния которая содержит количество лайков. Для того чтобы подвести итог тому, что мы, возможно, уже знаем о React, - это специальная функ­ React, такие как методы оп­ отметим, что хук ция, которая позволяет нам использовать особенности ределения состояния и жизненного цикла, в рамках функциональных компонентов. 6 Хук (hook) - ности React нововведение в React, которое позволяет использовать состояние и другие возмож­ без написания классов. - Прю1. пер.
Глава 54 1 Хуки позволяют нам повторно оперировать логикой с отслеживанием состояния без изменения иерархии компонентов, упрощая извлечение состояний. Хуки могут совместно использоваться компонентами. Ими удобно делиться с сообществом в виде автономных пакетов с открытым исходным кодом. Мы также определили функцию handleL i.ke, которая при нажатии на кнопку увели­ чивает количество лайков на единицу. Наконец, мы отображаем кнопку личество лайков с помощью Like и ко­ JSX. Теперь давайте подробнее рассмотрим, как работает виртуальный DOM в этом примере. При первом отображении компонента Li.keButton React создает виртуальное дерево DOM, которое отражает фактическ?е дерево DOM. Виртуальный DOM содержит один элемент di.v, в который входят элементы button и р: { $$typeof: Syмbol.for('react.eleмent'), type: 'di.v', ргорs: {}, chi. ldren: [ { $$typeof: SyмЬol.for('react.eleмent'), type : ' button ' , ргорs: { onCli.ck: handleli.ke }, chi.ldren: ['Li.ke'] }, { $$typeof: Syмbol.for('react.eleмent'), type: 'р', props: {}, chi.ldren: [0, ' Li.kes' ] } } Свойство chi.ldren элемента р содержит значение переменной состояния Li.kes, кото­ рая изначально имеет нулевое значение. Когда пользователь нажимает кнопку обновляет переменную состояния рево DOM, Like, вызывается функция handleli.ke, которая l i.kes. Затем React создает новое виртуальное де­ отражающее обновленное состояние: { type: ' di. v ' , props: {},
Обзор для начинающих 55 chi.ldren: { type: 'button' , { onCli.ck: handleli.ke }, chi.ldren: ['Li.ke'] ргорs: }, { type: 'р'' ргорs: {}, chi.ldren: [1, ' Li.kes'] } } Обратите внимание, что виртуальное дерево раньше, но свойство l i.kes, значение согласованием chi.ldren измененное с (reconciliation), DOM содержит те же элементы, что и элемента р было обновлено, чтобы отразить новое 0 на 1. Далее следует процесс, называемый в в ходе которого новый vDOM React сравнивается со ста­ рым. Давайте вкратце обсудим этот процесс. После вычисления нового виртуального дерева DOM React выполняет процесс, на­ зываемый согласованием, чтобы понять различия между новым деревом и старым. Согласование- это процесс сравнения старого виртуального дерева виртуальным деревом DOM DOM с новым и определения того, какие части фактического необходимо обновить. Если вам интересно, как именно это делается, в DOM главе 4 об этом рассказывается более подробно. А пока давайте рассмотрим нашу кнопку Like. В нашем примере React сравнивает старое деревом DOM и обнаруживает, виртуальное дерево туальным что элемент р изменился: в частности, DOM с новым вир­ изменились его свойства или состояние, или и то и другое. Это позволяет мечать компонент как "грязный" updated). Затем React (dirty) с DOM, DOM, чтобы согласовать состояние нового и в конечном итоге обновляет фактический изменения, внесенные в виртуальный React React по­ (should Ье вычисляет минимальный эффективный набор обновлений, которые необходимо внести в реальный vDOM или "требующий обновления" DOM, чтобы отразить DOM, чтобы свести к DOM. обновляет только необходимые части фактического минимуму количество манипуляций с DOM. Такой подход намного быстрее и эф­ DOM при каждом малейшем изменении. фективнее, чем обновление всего дерева Виртуальный DOM стал мощным и вдохновляющим изобретением для современ­ ного Интернета, и новые библиотеки, такие как React и Infemo, внедрили его, как только его полезность была доказана в React,-Mы подробнее рассмотрим виртуаль­ ный DOM в главе 4, а пока давайте перейдем к следующему разделу.
Глава 56 1 Компонентная модель React настоятельно рекомендует "мыслить компонентами", т. е. разбивать ваше приложение на более мелкие части и для создания вашего приложения добавлять их в более крупное дерево. Компонентная модель именно она делает ♦ React таким - ключевая концепция в React, и мощным. Давайте поговорим об этом подробнее. Компонентная модель поощряет повсеместное повторное использование одного и того же компонента, так что, если что-то сломается, вы "почините" в одном месте, и исправление будет сделано везде. Это называется принципом (Don't Repeat Yourself- DRY "не повторяйтесь") и является ключевым понятием в создании программного обеспечения. Например, если у нас есть компонент кнопки, мы можем использовать его во многих местах нашего приложения, а ес­ ли нам нужно модифицировать стиль кнопки, достаточно сделать это в одном месте, и он будет изменен везде. ♦ React с легкостью отслеживает компоненты и способен на такие чудеса произ­ водительности, как запоминание, пакетная обработка и другие оптимизации. Он может снова и снова идентифицировать конкретные компоненты и отслеживать их обновления с течением времени. Это называется манипулированием ключами (keying). свойство Например, если у нас есть компонент Button, мы можем присвоить ему key, а React сможет отслеживать этот компонент Button с течением вре­ мени и "знать", когда его следует обновить, а когда пропустить обновление и, таким образом, вносить минимальные изменения в пользовательский интерфейс. Большинство компонентов имеют неявные ключи, но мы всегда можем сделать их явными, если захотим. ♦ Компонентная модель помогает нам разделять задачи и размещать логику ближе к тем частям пользовательского интерфейса, на которые она влияет. Например, если у нас есть компонент Regi.sterButton, мы можем поместить логику того, что происходит при нажатии кнопки, в тот же файл, что и компонент Regi.ster Button, вместо того чтобы переходить к другим файлам в поисках логики происходяще­ го при нажатии кнопки. Компонент Regi.sterButton будет заключать в себе более простой компонент Button, а компонент Regi.sterButton будет отвечать за обра­ ботку логики, т. е. за то, что происходит при нажатии на кнопку. Это называется композицией. Компонентная модель React - это фундаментальная концепция, которая обеспечи­ вает популярность и успех платформы. Такой подход к разработке имеет множест­ во преимуществ, включая повышенную модульность, упрощение отладки и более эффективное повторное использование кода. Неизменяемое состояние В философии проектирования React особое внимание уделяется парадигме, в соот­ ветствии с которой состояние нашего приложения описывается как набор неизме­ няемых значений. Каждое обновление состояния рассматривается как новый от­ дельный снимок и ссылка на память. Этот подход к управлению состоянием
Обзор для начинающих является важной особенностью React 57 и обладает рядом преимуществ для разработ­ ки надежных, эффективных и предсказуемых пользовательских интерфейсов. Неизменяемость в React гарантирует, что компоненты пользовательского интер­ фейса отражают определенное состояние в любой момент времени. Когда состоя­ ние изменяется, вместо того чтобы изменять его напрямую, вы возвращаете новый объект, представляющий новое состояние. Это упрощает отслеживание изменений, отладку и понимание поведения вашего приложения. Поскольку переходы между состояниями являются дискретными и не мешают друг другу, вероятность появле­ ния несущественных ошибок, вызванных общим изменением состояния, значи­ тельно снижается. В следующих главах мы рассмотрим, как React пакетно обновляет состояние и об­ рабатывает его асинхронно для оптимизации производительности. Поскольку со­ стояние должно обрабатываться как неизменяемое, эти "транзакции" можно безо­ пасно агрегировать и применять без риска того, что одно обновление приведет к искажению процесса другого обновления. Это приводит к более предсказуемому управлению состоянием и может повысить производительность приложения, осо­ бенно при сложных переходах между состояниями. Использование неизменяемого состояния еще больше укрепляет передовые методы разработки программного обеспечения. Это побуждает разработчиков функцио­ нально подходить к потоку данных, уменьшая побочные эффекты и упрощая вы­ полнение кода. Ясность неизменяемого потока данных упрощает ментальную мо­ дель для понимания того, как работает приложение. Неизменяемость также предоставляет разработчику мощные инструменты, такие как отладка во времени с помощью Replay.io, посредством которого разработчики могут перемещаться вперед и назад по изменениям состояния приложения, чтобы проверять пользовательский интерфейс в любой момент времени. Это возможно только в том случае, если каждое обновление состояния сохраняется в виде уни­ кального и неизмененного моментального снимка. Приверженность React к обновлениям неизменяемых состояний - это осознанный выбор дизайна, который приносит множество преимуществ. Он соответствует со­ временным принципам функционального программирования, позволяя эффективно обновлять пользовательский интерфейс, оптимизируя производительность, снижая вероятность появления ошибок и улучшая общий опыт разработчиков. Такой под­ ход к управлению состоянием лежит в основе многих расширенных функций и будет оставаться основополагающим по мере дальнейшего развития Выпуск React React. React Однонаправленный поток данных был радикальным отходом от того способа, ко­ торым мы в течение многих лет создавали веб-приложения, и сначала был встречен скептически. Но Facebook был крупной компанией с мощными ресурсами, боль­ шим количеством пользователей и инженеров, обладающих собственным мнением, и это способствовало быстрому росту популярности React. После тщательного изу-
Глава 58 1 чения React получил одобрение Facebook, а затем Instagram. Затем, в году, 2013 React внутри некоторых компаний. Он был принят был выпущен в мир и его исходный код стал доступен всем, однако поначалу он был встречен с огромной негативной реакцией. Люди резко критиковали на JavaScript" React за использование JSX, обвиняя Facebook в "замене HTML Facebook стал известен и нарушении принципа разделения интересов. как компания, которая "переосмысливает лучшие практики" и взламывает Интер­ нет. В конце концов, после медленного и неуклонного внедрения такими компа­ ниями, как Netflix, Airbnb и New У ork Тimes, React стал стандартом де-факто для создания пользовательских интерфейсов в Интернете. В этой истории опущен ряд деталей, поскольку они выходят за рамки данной кни­ ги, но прежде чем мы углубимся в детали, важно понять контекст React: React был создан именно для решения вышеупомянутых технических проблем. Если вас больше заинтересует история React, полный документальный фильм об истории У ouTube в свободном доступе по названию "React.js: The Documentary" от Honeypot. этого фреймворка Учитывая, что блем, React доступен Facebook на был в первых рядах по решению этих масштабных про­ впервые применил основанный на компонентах подход к созданию пользовательских интерфейсов, который позволил решить эти задачи и многое дру­ гое. Каждый компонент представляет собой автономную единицу кода, которую можно повторно использовать и сочетать с другими компонентами для создания более удобного пользовательского интерфейса. Через год после того, как React был выпущен в виде программного обеспечения с Facebook представил Flux - шаблон для управления приложениях React. Flux был ответом на вызов, связанный с открытым исходным кодом, потоком данных в большими усилиями по управлению потоком данных в крупномасштабных прило­ жениях. Flux стал ключевой частью экосистемы на то, как он вписывается в Архитектура Flux - Давайте взглянем на Flux и Flux это архитектурный шаблон для создания клиентских веб-приложений, по­ пуляризируемый ется React. React. Facebook (теперь Meta) (рис. 1.3). В нем особое внимание уделя­ однонаправленному потоку данных, что делает поток данных в приложении более предсказуемым. Дмспmер Рис. 1.3. Архитектура Flux Представление
Обзор для начинающих Вот ключевые концепции архитектуры Действия 59 Flux. (actions). Действия - это простые объекты, содержащие новые данные и свойство, иден­ тифицирующее тип. Они представляют внешние и внутренние входные данные для системы, такие как взаимодействия с пользователем, ответы сервера и вход­ ные данные форм. Действия отправляются через центральный диспетчер в раз­ личные хранилища: Пример обьек~а дейс~вия // { type : ' ADD_TODO ' , text: 'Learn Flux Archi.tecture' } Диспетчер (dispatcher). Диспетчер является центральным узлом архитектуры Flux. Он принимает дейст­ вия и отправляет их в зарегистрированные хранилища приложения. Он управля­ ет списком обратных вызовов, и каждое хранилище регистрирует себя и свой обратный вызов в диспетчере. Когда действие диспетчеризируется, оно отправ­ ляется всем зарегистрированным обратным вызовам: // Пример диспе~черизации дейс~вия Di.spatcher.di.spatch(acti.on); Хранилища (stores). Хранилища содержат состояние и логику приложения. Они в некоторой степени похожи на модели в архитектуре MVC, но управляют состоянием многих объек­ тов. Они регистрируются у диспетчера и предоставляют обратные вызовы для обработки действий. Когда состояние хранилища обновляется, оно генерирует событие изменения, чтобы проинформировать представления о том, что что-то изменилось: // Пример хранилища class TodoStore extends constructor() { EventEмi.tter super(); thi.s.todos = []; } handleActi.ons(acti.on) { swi.tch (acti.on.type) { case "ADD_TODO" : thi.s.todos.push(acti.on.text); thi.s.eмi.t("change"); Ьгеаk; {
60 Глава 1 default: // нef'I операций } } } Представления (views). Представления - это компоненты React. Они отслеживают события изменений хранилищ и обновляются самостоятельно, когда меняются данные, от которых они зависят. Они также могут создавать новые действия для обновления состоя­ ния системы, формируя однонаправленный цикл потока данных. Архитектура Flux обеспечивает однонаправленный поток данных через систему, что упрощает отслеживание изменений с течением времени. Эта предсказуе­ мость может быть позже использована компиляторами в качестве основы для дальнейшей оптимизации кода, как в случае с React Forget (подробнее об этом позже). Преимущества архитектуры Архитектура Flux обеспечивает Flux множество преимуществ, которые помогают управлять сложностью веб-приложений и улучшают удобство их обслуживания. Вот некоторые из заметных преимуществ. Единый источник достоверности. Flux подчеркивает важность наличия единого источника достоверности для со­ стояния приложения, который хранится в хранилищах. Такое централизованное управление состоянием делает поведение приложения более предсказуемым и понятным. Оно устраняет сложности, связанные с наличием нескольких взаимо­ зависимых источников достоверности, которые могут привести к ошибкам и не­ согласованному состоянию приложения. Тестируемость. Четко определенные структуры Flux и предсказуемый поток данных обеспечи­ вают высокую степень тестируемости приложения. Разделение задач между раз­ личными частями системы (например, действиями, диспетчером, хранилищами и представлениями) позволяет проводить модульное тестирование каждой части изолированно. Более того, тесты проще писать, когда поток данных однонаправ­ лен и когда состояние хранится в определенных предсказуемых местах. Разделение задач (sepaгation of"conceгns, Flux SoC). четко разделяет задачи различных частей системы, как описано ранее. Та­ кое разделение делает систему более модульной, более простой в обслуживании и более понятной для анализа. Каждая часть имеет четко определенную роль, а однонаправленный поток данных позволяет понять, как эти части взаимодейст­ вуют друг с другом.
Обзор для начинающих Архитектура Flux 61 обеспечивает прочную основу для создания надежных, масшта­ бируемых и поддерживаемых веб-приложений. Ее акцент на однонаправленный поток данных, единый источник достоверности и разделение задач позволяет соз­ давать приложения, которые проще разрабатывать, тестировать и отлаживать. Подведение итогов: почему React - React - это стоящая вещь? это нечто особенное, потому что он дает возможность разработчикам соз­ давать пользовательские интерфейсы с большей предсказуемостью и надежностью, позволяя нам декларативно выражать то, что мы хотели бы видеть на экране. React заботится о том, как это сделать, эффективно внося постепенные обновления в DOM. Это также побуждает нас мыслить компонентно, что помогает нам разде­ лять задачи и легче использовать код повторно. Он протестирован в Meta и предна­ значен для масштабного использования. Кроме того, он имеет открытый исходный код и бесплатен для использования. React также обладает обширной и активной экосистемой с широким спектром ин­ струментов, библиотек и ресурсов, доступных разработчикам. Эта экосистема включает в себя инструменты для тестирования, отладки и оптимизации приложе­ ний, а также библиотеки для выполнения таких распространенных задач, как управление данными, маршрутизация и управление состоянием. Кроме того, сооб­ щество React активно участвует в разработке и поддерживает общение благодаря множеству онлайн-ресурсов, форумов и сообществ, которые помогают разработчи­ кам учиться и расти. React не зависит от платформы, что означает, что его можно использовать для соз­ дания веб-приложений на широком спектре платформ, включая настольные ком­ пьютеры, мобильные устройства и виртуальную реальность. Такая гибкость делает React привлекательным вариантом для разработчиков, которым необходимо созда­ вать приложения сразу для нескольких платформ, поскольку он позволяет им ис­ пользовать единую кодовую базу для создания приложений, работающих на разно­ образных устройствах. Подводя итог, можно сказать, что ценность React основана на его компонентной архитектуре, декларативной модели программирования, виртуальном обширной экосистеме, независимости от платформы и поддержке ности эти функции делают React Meta. DOM, JSX, В совокуп­ привлекательным вариантом для разработчиков, которым необходимо создавать быстрые, масштабируемые и поддерживаемые веб­ приложения. Независимо от того, создаете ли вы простой веб-сайт или сложное корпоративное приложение, React может помочь вам достичь ваших целей более эффективно, чем многие другие технологии. Давайте подведем итоги этой главы.
Глава 62 1 Обзор главы В этой главе мы рассмотрели краткую историю React, определили, в чем его цен­ ность, и показали, как он решает проблемы небезопасных, непредсказуемых и не­ эффективных масштабных обновлений пользовательского интерфейса. Мы также поговорили о компонентной модели и о том, почему она стала революционной для веб-интерфейсов. Давайте подытожим то, чего мы достигли. В идеале, после про­ чтения этой главы вы стали лучше осведомлены о корнях React и о том, откуда он взялся, а также о его основных преимуществах. Проверьте ваши знания Давайте убедимся, что вы полностью усвоили темы, которые мы затронули. Найди­ те минутку, чтобы ответить на следующие вопросы: React? 1. Что послужило мотивацией для создания 2. Как 3. Что такого особенного в архитектуре 4. В чем преимущества декларативных абстракций программирования? 5. Какова роль виртуального React улучшает предыдущие DOM шаблоны, такие как MVC и MVVM? Flux? в создании эффективных обновлений пользова­ тельского интерфейса? Если у вас возникли трудности с ответом на эти вопросы, возможно, стоит прочи­ тать эту главу еще раз. Если нет, давайте перейдем к следующей главе. Что дальше? В главе 2 мы немного углубимся в декларативную абстракцию, которая позволяет нам выразить то, что мы хотим видеть на экране: синтаксис и внутреннюю работу JSX - языка, похожего на HTML в JavaScript. JSX доставлял React много проблем на заре его существования, но в конечном итоге стал идеальным способом создания пользовательских интерфейсов в Интернете, оказав влияние на ряд новых библио­ тек пользовательских интерфейсов.
ГЛАВА 2 JSX В главе 1 мы познакомились с основами и историей создания другими популярными библиотеками узнали, в чем ценность React JavaScript React, сравнив его с и фреймворками того времени. Мы и почему эта технология сегодня занимает важное ме­ сто. В этой главе мы узнаем о рение JavaScript, JSX, который представляет собой синтаксическое расши­ позволяющее нам писать НТМL-подобный код внутри нашего JavaScript-кoдa. Когда в 2013 году был представлен React, это было первое, что лю­ ди заметили и что подверглось резкой критике, поэтому имеет смысл разобраться с данным вопросом в начале книги. Теперь давайте углубимся в это расширение язы­ ка, посмотрим, как работает JSX и как мы можем создать собственный код. Для начала давайте разберемся, что означает JSX. Мы уже знаем, что JS - это JavaScript версии l О? Как в Мае OS Х? можем подумать, что Х в JSX означает 10 или Xtra, что бы­ ло бы хорошим предположением! Но Х здесь означает расширение, а JSX это сокращение JavaScript Syntax eXtension, т. е. расширение синтаксиса JavaScript. Его также иногда называют JavaScript XML. JavaScript. Означает ли Или это JSXtra? Да, мы это, что JSX - это Является ли JSX симбиозом JavaScript и XML? Если вы уже некоторое время работаете в Интернете, то, возможно, помните тер­ мин AJAX, или асинхронный JavaScript появившийся примерно в 2000-х годах. и XML (Asynchronous JavaScript and XML), AJAX, по сути, был новым способом ис­ пользования существующих технологий для создания высокоинтерактивных веб­ страниц, которые обновлялись на месте и асинхронно, в отличие от традиционной загрузки новой страницы при каждом изменении состояния. Используя такие инструменты, как XМLHttpRequest в браузере, асинхронный (т. е. неблокирующий) запрос по протоколу НТТР Protocol в формате AJAX инициировал (HyperText Transfer протокол передачи гипертекста). На этот запрос традиционно был ответ XML. Сегодня мы, как правило, вместо этого используем JSON. Вероятно,
64 Глава 2 это одна из причин, по которой функция поскольку в самом имени запроса fetch победила интерфейс XMLHttpRequest присутствует XML. это синтаксическое расширение JSX - JavaScript, XМLHttpRequest, которое позволяет разработчи­ кам включать НТМL-подобный код в свой JavaScript-кoд. Первоначально оно было разработано компанией Meta для использования совместно с было принято и другими библиотеками и фреймворками. язык, а скорее расширение синтаксиса, которое React, JSX - но с тех пор это не отдельный преобразуется в обычный JavaScript-кoд с помощью компилятора или транспилятора. Таким образом, когда JSХ-код компилируется, он преобразуется в обычный JavaScript-кoд. Подробнее об этом мы поговорим позже. Хотя синтаксис мер, JSX JSX похож на есть несколько ключевых отличий. Напри­ HTML, использует фигурные скобки {} для встраивания выражений НТМL-подобный код. Кроме того, атрибуты JSX записываются в HTML, например: oncli.ck гистре JavaScript в верблюжьем ре­ в HTML- это (camelCase) вместо атрибутов в JSX. Элементы HTML записываются в нижнем регистре, в то время как пользовательские элементы или компоненты JSX пишутся в стиле Тitle Case (с за­ главной буквы), например: di.v - это HTML, а Di.v - это уже компонент React. onCli.ck Далее мы должны упомянуть, что можно создавать приложения зования JSX, React без исполь­ но в этом случае код, как правило, становится трудным для чтения, осмысления и поддержки. Тем не менее, если мы захотим, то можем это сделать. Давайте в качестве иллюстрации рассмотрим компонент мощью JSX React, и без него. Вот пример списка, созданного с помощью const MyCOl'lponent = () => ( <secti.on i.d="li.st"> <h1>Thi.s i.s му li.st!</h1> <p>Isn't му li.st aмazi.ng? It contai.ns <Ul> {aмazi.ngThi.ngs.мap((t) => ( <li. key={t.i.d}>{t.label}</li.> JSX: aмazi.ng thi.ngs!</p> ))} </ul> </secti.on> ); Теперь пример того же списка без JSX: const MyCOl'lponent = () => React.createEleмent( "secti.on" , { i.d: "li.st" } , React.createEleмent("hl", React.createEleмent( {}, "Thi.s i.s му li.st!"), выраженный с по­
JSX 1 65 "р", {}' "Isn't 111у li.st al'lё!zt.ng? It contat.ns tht.ngs!" al'lё!zt.ng ), React.createElel'lent( "ul", {}' a111azt.ngTht.ngs.l'lё!p((t) => React.createElel'lent("li.", { key: t.t.d }, t.laЬel) ) ); Для большей наглядности мы использовали более раннее преобразование бы проиллюстрировать, как React был написан без использования JSX. JSX, что­ Позже в этой главе мы подробно рассмотрим преобразования синтаксиса, а пока давайте примем на веру, что преобразование- это то, что преобразует синтаксис А в син­ таксис Б. В настоящее время в React 17, React поставляется с новым преобразованием, представленным которое автоматически импортирует некоторые специальные функции, позволяющие, по сути, делать то же самое. Это незначительная· деталь в общей схеме вещей, но с более новым преобразованием мы бы представили список без JSX следующим образом: vipoгt { jsx as _jsx} fгOl'I "react/jsx-runt'irle"; vipoгt { jsxs as _jsxs} fгOl'I "react/jsx-runt'irle"; const MyCOl'lponent = () => _jsxs("sectt.on", { t.d: "li.st", cht.ldren: [ _jsx("hl", { cht.ldren: "Tht.s t.s 111у lt.st!", }), _jsx("p", { chHdren: "Isn't 111у li.st al'lё!zt.ng? It contat.ns }), _jsx("ul", { cht.ldren: al'lё!zt.ngTht.ngs.l'lё!p((t) => _jsx( "li..", al'lё!zt.ng tht.ngs!",
Глава 66 2 { children: t.laЬel, }, t.id ) )' }), ], }); В любом случае, видите ли вы разницу между примерами с использованием без него? Первый пример с использованием JSX лее удобным для чтения и сопровождения, чем второй. Первый рой - vanilla JS. Давайте поговорим о компромиссах Преимущества Использование JSX JSX и может показаться вам гораздо бо­ - это JSX, вто­ JSX. JSX в веб-разработке имеет следующие преимущества. Упрощенные чтение и запись. Синтаксис чтения и записи в комых с JSX стал проще, особенно для разработчиков, зна­ HTML. Повышенная безопасность. JSХ-код можно скомпилировать в более безопасный JavaScript-кoд, который -создает НТМL-строки с удаленными потенциально опасными символами, таки­ ми как < и >, которые могут создавать новые элементы. Вместо этого в этих НТМL-строках угловые скобки заменяются знаками "меньше" и "больше", что­ бы сделать работу более безопасной. Этот процесс называется санацией. Строгая типизация. В JSX предусмотрена строгая типизация, которая помогает выявлять ошибки до JSX может быть выражен с помощью TypeScript он все равно может выиграть их возникновения. Это связано с тем, что TypeScript, но даже без использования от повышения безопасности типов за счет использования комментариев в стиле JSDoc и propTypes. Поощрение компонентной архитектуры. JSX поддерживает компонентную архитектуру, которая может помочь сделать код более модульным и простым в обслуживании. Широкое использование. JSX широко используется в сообществе библиотеками и фреймворками. React, а также поддерживается другими
JSX Недостатки Использование 1 67 JSX JSX имеет и некоторые недостатки. Процесс обучения. Разработчики, которые не знакомы с JSX, могут столкнуться с трудностями в его изучении и понимании. Требуется компиляция. Прежде чем код JSX может быть выполнен, его необходимо скомпилировать в JavaScript, что добавляет дополнительный шаг к процессу разра­ Другие альтернативы, такие как Vue.js, например, могут сразу же рабо­ обычный код ботки. тать в среде браузера, если они включены на страницу в виде тега <scri.pt>. Смешивание подходов. Некоторые разработчики утверждают, что JSX смешивает разнородные подхо­ ды, комбинируя НТМL-подобный код с JavaScript-кoдoм, что, по их мнению, за­ трудняет отделение представления от логики. Частичная совместимость с JSX JavaScript. поддерживает встроенные выражения, но не встроенные блоки. То есть внутри дерева элементов JSX у нас может быть встроенное выражение, но не блоки типа i.f или swi.tch. Это может вызвать затруднения у разработчиков, не привыкших работать с JSX. Несмотря на свои недостатки, JSX стал популярным выбором для веб-разработ­ чиков, в частности для тех из нас, кто работает с React. Он предлагает мощный и гибкий способ создания компонентов и пользовательских интерфейсов. JSX был одобрен и поддерживается большим и активным сообществом. Помимо использо­ вания в React, JSX также применяется в других библиотеках и фреймворках, вклю­ чая Vue.js, Solid, Qwik и др. Это говорит о том, что JSX имеет более широкое при­ менение, чем его реализация в React, и его популярность, вероятно, продолжит расти в ближайшие годы, даже если он выйдет за рамки React и веб-экосистемы и распространит свое влияние на такие реализации, как SwiftUI в пространстве IOS и другие области. В целом JSX- это мощный и гибкий инструмент, который может помочь нам соз­ давать динамичные и отзывчивые пользовательские интерфейсы. для решения одной задачи компонентов React, - JSX был создан сделать презентабельным и поддерживаемым код для сохранив при этом такие мощные возможности, как итерация, вычисления и встроенное выполнение. JSX становится обычным JavaScript еще до того, как попадает в браузер. Как это достигается? Давайте заглянем под капот!
Глава 68 2 Что "под капотом"? Как создать языковое расширение? Как оно работает? Для того чтобы ответить на эти вопросы, нам потребуется немного разобраться в языках программирования. В частности, нам нужно изучить, как именно нижеследующий код выводит значе­ ние З: const а= 1; tet Ь = 2; console.log(a + Ь); Уяснение этого позволит нам лучше понять лучше понять React, тем самым JSX, что, в свою очередь, поможет нам повысив на~µи навыки в работе с ним. Как работает код? Фрагмент кода, который приведен выше, представляет собой буквально текст. Как это интерпретирует компьютер, а затем выполняет? Начнем с того, что несложно написать регулярное выражение RegExp, которое позволит идентифицировать клю­ чевые слова в текстовом файле. Однажды я таким образом попытался создать язык программирования и потерпел сокрушительную неудачу, потому что регулярные выражения часто трудно составить правильно, еще труднее их прочитать и мыс­ ленно проанализировать, и их довольно сложно поддерживать из-за проблем с удо­ бочитаемостью. Например, ниже приводится регулярное выражение для идентифи­ кации корректного адреса электронной почты. На первый взгляд, практически невозможно определить его назначение: \[(?:[а-z0-9!#\$%&'\*\+-/=\?\л_'{\1}-]+(?:\.[а-z0-9!#\$%&'\*\+-/=\?\л_-{\1}-]+)\ *l"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]l\\[\x01-\x09\x0b\x0c\x0 e-\x7f])\*")@(?:(?:[a-z0-9](?:[a-z0-9-]\*?[a-z0-9])?\.)\*?[a-z0-9](?:[a-z0-9-]\* ?[а-z0-9])?1\[(?:(?:25[0-5]12[0-4][0-9]1[01]?[0-9][0-9]?)\.){З}(?:25[0-5]12[0-4] [0-9]l[01]?[0-9][0-9]?1[a-z0-9-]\*?[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\ x5a\x53-\x7f]l\\[\x01-\x09\x0b\x0c\x0e-\x7f])\+)\])\] Это регулярное выражение даже не является полностью корректным, потому что его полная версия не помещается на странице! Вот почему вместо использования регулярных выражений код компилируется с помощью кoмnWlЯmopa. Компиля­ тор - это часть программного обеспечения, которая преобразует исходный код, написанный на языке программирования высокого уровня, в синтаксическое дерево (буквально, древовидную структуру данных, подобную объекту JavaScript) в соот­ ветствии с определенными правилами. Процесс компиляции кода имеет несколько этапов, включая лексический анализ, синтаксический разбор, семантический ана­ лиз, оптимизацию и генерацию кода. Давайте рассмотрим каждый из этих этапов более подробно и обсудим роль компиляторов в современной среде разработки программного обеспечения.
JSX 69 Компилятор использует описанный здесь трехэтапный процесс (по крайней мере, в JavaScript). Эти этапы называются токенизацией 1 , парсингом и генерацией кода. Давайте рассмотрим каждый из этих этапов более подробно. Токенизация. По сути, это разбиение строки символов на значимые отрезки - токены. Когда токенизатор сохраняет состояние и каждый токен содержит данные о родителе и/или дочерних элементах, токенизатор называется лексером (lexer), или лексическим анализатором. Это необходимое упрощение для целе~ нашего об­ суждения здесь: лексизация - это, по сути, токенизация с учетом состояния. У лексеров есть лексические правила, которые в отдельных случаях действи­ тельно используют регулярное выражение или что-то подобное для обнаруже­ ния в текстовой строке ключевых токенов, таких как имена переменных, ключи и значения объектов и многое другое. Затем лексер отображает эти ключевые слова на некоторый тип перечисляемого значения, в зависимости от его реали­ зации. Например, const становится 0, let-1, functi..on - 2и т. д. Как только строка преобразована в токен или лексему, мы переходим к следую­ щему шагу - парсингу. Парсинг. Это процесс синтаксического анализа и преобразования токенов в синтаксиче­ ское дерево. Синтаксическое дерево - это структура данных, которая представ­ ляет структуру кода. Например, фрагмент кода, который мы рассматривали ра­ нее, можно представить в виде синтаксического дерева так: { type: "Progra111", Ьоdу: { type: "Vari..aЫeDeclarati..on", declarati..ons: [ { type: "Vari..aЫeDeclarator", i..d: { type: "Identi..fi..er", nаме: "а" }, i..ni..t: { type: "Li..teral", value: 1, raw: "1" 1 Токенизация (tokenization) - процесс символов на распознанные группы - аналитического лексемы последовательностей, называемых токенами. - - разбора входной последовательности с целью получения на выходе идентифицированных Прим. пер.
70 Глава 2 } } ], ki.nd: "const" }, { type: "Vari.aЫeDeclarati.on", declarati.ons: [ { type: "Vaгi.aЫeDeclarator", i.d: { type: "Identi.fi.er", narrie: "Ь" }, i.ni.t: { type: "Li.teral", value: 2, гаw: "2" } } ], ki.nd: "let" }, { type: "Expressi.onState111ent" , expressi.on: { type: "CallExpressi.on", callee: { type: "Identi.fi.er", nar,e: "console" }, argul'lents: [ { type: "Bi.na ryExpressi.on" , left: { type: "Identi.fi.er", narrie: "а" }, ri.ght: { type: "Identi.fi.er", narrie: "Ь"
JSX 1 71 }, operator: "+" } ] } } ] } Строка, благодаря синтаксическому анализу (парсингу), становится объектом Когда у нас, программистов, есть такая структура данных, мы можем де­ JSON. лать действительно интересные вещи. Языковые движки используют эти струк­ туры данных для завершения процесса на третьем этапе - генерации кода. Генерация кода. Здесь компилятор генерирует машинный код из абстрактного синтаксического дерева (abstract syntax tree, AST). Этот процесс включает в себя преобразование исходного кода в последовательность инструкций, которые могут быть выпол­ нены непосредственно процессором компьютера. Полученный машинный код затем выполняется движком JavaScript. В целом процесс преобразования AST в машинный код сложен и включает в себя множество различных этапов. Однако современные компиляторы отличаются высокой сложностью и могут создавать высоко оптимизированный код, который эффективно работает на широком спектре аппаратных архитектур. Существует несколько типов компиляторов, каждый из которых отличается своими характеристиками и вариантами использования. К наиболее распространенным ти­ пам компиляторов относятся следующие. Нативные компиляторы. Эти компиляторы создают нативный (от англ. native- естественный, родной) машинный код, который может быть выполнен непосредственно процессором целевой платформы. Нативные компиляторы обычно используются для создания автономных приложений или программного обеспечения системного уровня. Кросс-компиляторы. Эти компиляторы создают машинный код для платформы, отличной от той, на которой запущен компилятор. Кросс-компиляторы часто используются при раз­ работке встраиваемых систем или при работе со специализированным оборудо­ ванием. ЛТ-компиляторы (Just-in-Тime compilers). Эти компиляторы отличаются тем, что преобразуют код в машинный во время выполнения программы, а не заблаговременно. ЛТ-компиляторы широко при­ меняются в виртуальных машинах, таких как Java virtual machine, и могут обес­ печить значительные преимущества в производительности по сравнению с тра­ диционными интерпретаторами.
72 Глава 2 Интерпретаторы. Такие программы выполняют исходный код напрямую, без компиляции. Интер­ претаторы обычно работают медленнее компиляторов, но обеспечивают боль­ шую гибкость и простоту использования. Для эффективного выполнения кода JavaScript во многих современных средах, включая веб-браузеры, используются ЛТ-компиляторы. В этих системах исходный код JavaScript может быть сначала преобразован в промежуточное представление, такое как байт-код. Затем ЛТ -компилятор динамически компилирует этот байт-код в машинный код непосредственно во время выполнения программы. Эта компиляция "на лету" позволяет движку выполнять оптимизацию на основе поступающей в реальном времени информации, такой как типы переменных и пути к часто выполняемому коду. Некоторые движки используют несколько этапов ком­ пиляции, начиная с быстрой неоптимизированной компиляции для мгновенного старта, за которой следует более оптимизированная компиляция для часто выпол­ няемых сегментов кода. Этот динамический подход позволяет движкам JavaScript достигать впечатляющей производительности для широкого спектра приложений. Среды выполнения (runtimes) обычно взаимодействуют с движками, чтобы предос­ тавить больше контекстных помощников (helpers) и функций. Наиболее популяр­ ной средой выполнения на сегодняшний день является обычный веб­ браузер, такой как он предоставляет среду выполнения JavaScript Google Chrome: Chromium, которая и взаимодействует с движком. Аналогичным образом, на стороне сервера мы используем среду выполнения жок v8. которая по-прежнему использует дви­ Node.js, Какие еще движки и среды выполнения вы можете выделить из сущест­ вующих сегодня? Среды выполнения предоставляют движкам wi.ndow и JavaScript контекст, такой как объект объект docuмent, с которыми поставляется среда выполнения браузера. Если вы работали с браузерами и Node.js ранее, вы, возможно, заметили, что Node.js не имеет глобального объекта wi.ndow. Это связано с тем, что это другая среда выполне­ ния и, как таковая, предоставляет другой контекст. Облачный сервис Cloudflare создал аналогичную среду выполнения под названием Workers, единственной обя­ занностью которой является выполнение JavaScript на глобально распределенных компьютерах, называемых еdgе-серверами. Bun и Deno являются альтернативными средами выполнения. Но мы немного отвлеклись. Как все это связано в JSX? Расширение синтаксиса JavaScript с помощью JSX Теперь, когда мы понимаем, как бы мы могли расширить синтаксис дет ли легче понять, как работает JavaScript, JSX? JavaScript, бу­ Для того чтобы расширить синтаксис нам нужно либо использовать другой движок, способный понимать наш новый синтаксис, либо подготовить нужным образом наш новый синтаксис до того, как он попадет в движок. Первое практически невозможно, т. к. для создания и об­ служивания движков требуется много усилий, поскольку они, как правило, широко используются. Если мы решим применить этот вариант, могут пройти годы или де­ сятилетия, прежде чем мы сможем начать использовать наш расширенный синтак-
JSX 1 73 сие! В этом случае нам пришлось бы убедиться, что наш "специальный движок, созданный на заказ" используется повсеместно. Как бы мы убедили производите­ лей браузеров и другие заинтересованные стороны перейти на нашу непопулярную новинку? Это вряд бы ли сработало. Второй путь практичнее: давайте посмотрим, как мы можем справиться с нашим новым синтаксисом до того, как он попадет в движок . Для того чтобы сделать это, нам нужно создать наши собственные лексический и синтаксический анализаторы, способные понимать наш расширенный язык, т. е. взять текстовую строку кода и понять ее. Тогда, вместо того чтобы генерировать машинный код, как это принято традиционно, мы можем взять наше синтаксическое дерево и вместо него сгенери­ ровать обычный JavaScript, давно понятный всем современным движкам . в экосистеме JavaScript, именно то, что делает компилятор Babel инструментами , такими как TypeScript, Traceur и swc (рис . 2.1 ). ФaйnJSX ФaйnJSX Новый движок JSX Препроцессор JSX (подобный Babel) Токинезацмя Против Это наряду с другими Токинезация Парсинr Парсинr Выполнение Трансформация ДвижокJS ФaйnJS Выполнение Рис. 2.1. Сравнение создания нового движка с использованием препроцессора Из-за этого JSX JSX JS нельзя использовать непосредственно в браузере, вместо этого тре­ буется "этап сборки", на котором пользовательский синтаксический анализатор об­ рабатывает код, а затем компилирует его в синтаксическое дерево . Далее этот код преобразуется в обычный JavaScript в виде окончательного распространяемого па­ кета. Это называется транспиляцией (transpilation): код сначала преобразуется, а затем компилируется . Для того чтобы было понятно , транспиляция - это процесс перевода исходного кода, написанного на одном языке, на другой язык, который имеет аналогичный уровень абстракции . Вот почему он так же известен как компиляция исходного кода в исходный код (source-to-source compilation).
Глава 74 2 Формально это разновидность транслятора. Этот общий термин может относиться, например, к компилятору, ассемблеру или интерпретатору. Концептуально этот процесс почти идентичен компиляции, за исключением того, что целевой язык на­ ходится на том же уровне абстракции, что и исходный язык. Например, TypeScript - это язык высокого уровня, который при транспиляции преобразуется в JavaScript (также язык высокого уровня). Другой пример- пере­ нос JavaScript-кoдa ЕSб в JavaScript ES5, выполненный с помощью компилятора Babel. Теперь, когда мы понимаем, как создать наше собственное расширение давайте посмотрим, что мы конкретно можем сделать с расширением JavaScript, JSX. JSX-nparмa Все начинается с символа <, который сам по себе является неузнаваемым символом в JavaScript, когда используется вне операций сравнения. Когда движок JavaScript сталкивается с ним, он выдает синтаксическую ошибку: "SyntaxError: Unexpected token '<'" ("Синтаксическая ошибка: неожиданный токен '<' "). В JSX эта "JSX pragma" может быть транспилирована в вызов функции. Прагма директива компилятору, предоставляющая (pragma) - ему дополнительную это информацию, обычно выходящую за рамки того, что передается в самом языке. Прагма может, например, указать компилятору, как он должен обрабатывать некоторый специфи­ ческий отрывок кода или файла. Примерами этого в JavaScript являются прагмы "use stri.ct", которые мы иногда видим в начале кода старых модулей, и недавняя прагма "use cli.ent" в контексте React Server Components (RSC). Подробнее о них см. в главе 9. Имя функции, вызываемой при обнаружении синтаксическим анализатором прагмы <, может настраиваться, а по умолчанию используется функция React. createElel'lent или _jsxs с более новым преобразованием. Сигнатура этой функции будет такой: functi.on pragl'IO(tag, props, ... children) То есть она получает tag, props, и chi.ldren в качестве аргументов. Вот как транспилируется в обычный синтаксис <МyCOl'lponent JavaScript. Следующий код JSX JSX: prop="value">contents</MyCOl'lponent> становится в коде JavaScript такой строкой: React.createEle1'1ent(MyCOfl1ponent, { ргор: "value" }, "contents"); Обратите внимание на соответствие между тегом (MyC01'1ponent), атрибутом (prop="value") и дочерними элементами ("contents"). По сути, эта роль прагмы JSX - синтаксический сахар 2 для множественных рекурсивных вызовов функций. Прагма JSX 2 фактически является псевдонимом React. createEle111ent. Синтаксический сахар (syntax sugar) в .языке программирования - синтаксические возможности, применение которых не влияет на поведение программы, но делает использование .языка более удобным для человека. - Прим. пер.
JSX 1 75 Выражения Одной из самых мощных функций JSX является возможность выполнения кода внутри дерева элементов. Для того чтобы выполнить итерацию по списку, как мы делали в разд. "Что "под капотом"?" ранее в этой главе, мы можем заключить исполняемый код в фигурные скобки, как мы делали при помощи l'lap ранее в этой главе. Если мы хотим вычислить сумму двух чисел в JSX, мы сделаем это следую­ щим образом: const const а= Ь 1; = 2; const MyCol'lponent = () => an expresston: {а+ Ь}</Вох>; "Неге' s an expresston: 3", поскольку <Вох>Неге's Это приведет к отображению данные, заклю­ ченные в фигурные скобки, выполняются как выражение. Используя выражения JSX, мы можем перебирать списки и выполнять самые различные выражения, включая условные проверки с помощью тернарных операций, замену строк и мно­ гое другое. Вот еще один пример с условной проверкой и использованием тернарной опера­ ции: const const а= Ь 1; = 2; const MyCOl'lponent =О=> <Вox>Is Ь l'IOГe than а? {Ь >а? Этот фрагмент кода приведет к отображению "Is Ь "YES" : l'IOre than "NО"}</Вох>; а? YES", поскольку сравнение представляет собой вычисляемое выражение. Для дальнейшего исполь­ зования стоит упомянуть, что выражения JSX - это именно выражения. Невоз­ можно выполнить инструкции внутри дерева элементов JSX. Следующий код не сработает: const MyCOl'lponent = () => const а= 1; const Ь = 2; i.f (а> Ь) <Вох>Неге's an expresston: { { 3 } }</Вох>; Это не работает, потому что инструкции ничего не возвращают и считаются побоч­ ными эффектами: они устанавливают состояние, не выдавая значения. Как бы мы вывели значение в строке после инструкций и вычислений? Обратите внимание, что в примере мы просто поместили число З в строку 6. Как наш рендерер должен узнать, что мы собираемся вывести з? Вот почему выражения вычисляются, а опе­ раторы - нет.
76 Глава 2 Обзор главы Итак, мы достаточно подробно рассмотрели тему JSX. Теперь мы должны чувство­ вать себя достаточно уверенно (или даже свободно, если хотите) в этой теме, чтобы объяснять все ее аспекты. Проверьте ваши знания Давайте убедимся, что вы полностью усвоили темы, которые мы затронули. Найди­ те минутку, чтобы ответить на следующие вопросы: 1. Что такое 2. В чем разница между 3. Как текстовая строка превращается в машинный код? 4. Что такое выражения JSX? В чем его достоинства и недостатки? JSX и HTML? JSX и какие преимущества они дают? Если у вас возникли трудности с ответом на эти вопросы, возможно, стоит прочи­ тать эту главу еще раз. Если нет, давайте перейдем к следующей главе. Что дальше? Теперь, когда мы довольно свободно чувствуем себя в внимание на следующий аспект React JSX, давайте обратим наше и посмотрим, как мы можем выжать из него максимум знаний, чтобы еще больше расширить свою навыки. Давайте перейдем к рассмотрению виртуального DOM.
ГЛАВА Виртуальный В этой главе мы подробно рассмотрим концепцию виртуального обозначают еще как React vDOM, и значение использует виртуальный DOM vDOM в React. DOM, 3 DOM которую Мы также выясним, как для упрощения и повышения эффективности веб-разработки. По мере усложнения веб-приложений управлять "реальным DOM" становится все труднее, потому что это сложный и подверженный ошибкам процесс, как мы вско­ ре увидим и как мы в общих чертах уже рассмотрели в главе в React предлагает решение 1. Виртуальный DOM этой проблемы. DOM, способы его реализации DOM. Мы также изучим, как React оп­ DOM с помощью виртуального DOM и В данной главе мы рассмотрим работу виртуального в React и его преимущества перед реальным тимизирует производительность реального как эти две модели сочетаются. С помощью серии примеров кода и подробных объяснений вы поймете роль вирту­ ального DOM в React и узнаете, как использовать его преимущества для создания надежных и эффективных веб-приложений. Давайте начнем! Введение в виртуальный DOM JavaScript-DOM, представляет собой НТМL­ документ, созданный в виде объекта JavaScript. DOM буквально означает объект­ ную модель документа (document object model). Сам DOM является моделью доку­ мента, используемой браузером. Виртуальный DOM - упрощенная копия этого, с тем ключевым отличием, что в то время как реальный DOM состоит из объектов Node (узлов), виртуальный DOM состоит из простых объектов JS, которые действу­ Виртуальный DOM, как и реальный ют как описания. Это позволяет веб-разработчикам создавать пользовательские ин­ терфейсы более эффективным и производительным способом, о чем мы узнаем в этой главе. В React всякий раз, когда мы приказываем внести изменения в пользовательский интерфейс с помощью setState или какого-либо другого механизма, сначала моди­ фицируется виртуальный DOM, а затем и реальный DOM обновляется в соответст-
Глава 78 3 вии с изменениями в виртуальном (reconciliation), DOM. 4. Этот процесс называется согласованием и ему посвящена глава Причина, по которой сначала обновляется виртуальный что модификация реального DOM DOM, заключается в том, может быть медленным и дорогостоящим про­ цессом. Мы рассмотрим это в следующем разделе, но суть в том, что каждый раз, когда в реальный DOM вносятся изменения, браузеру приходится пересчитывать макет страницы, перерисовывать экран и выполнять другие операции, которые мо­ гут занять много времени. Например, простое считывание смещения элемента offsetWidth может вызвать пе­ ресчет 1 (reflow) страницы, при котором браузер пересчитывает макет всего доку­ мента или его части, что потенциально влияет на производительность и делает прямое взаимодействие с DOM менее эффективным: const btn = docuP1ent.getEler1entByid("P1yButton"); const width = btn.offsetWidth; // Эf1ю може~ вызва~ь пересче~ С другой стороны, обновление виртуального DOM происходит намного быстрее, поскольку оно не требует каких-либо изменений в фактическом макете страницы. Это простой объект JavaScript, которым можно быстро и эффективно манипулиро­ вать с помощью различных алгоритмических подходов, позволяющих наилучшим образом использовать движок JavaScript, и таким образом повышать его эффектив­ ность независимо от браузеров и других хост-сред. При внесении обновлений в виртуальный DOM React использует специальный ал­ горитм для определения различий между старой и новой версиями виртуального DOM. Этот алгоритм определяет минимальный набор изменений, необходимых для обновления реального DOM, и эти изменения применяются пакетным и оптимизи­ рованным способом, чтобы минимизировать влияние на производительность. В этой главе мы рассмотрим различия между виртуальным подводные камни реального DOM и то, как виртуальный DOM и реальным DOM, DOM помогает создавать лучшие пользовательские интерфейсы. Мы также рассмотрим реализацию вирту­ ального DOM в React и алгоритмы, которые используются для эффективного об­ новления. Реальный DOM Когда НТМL-страница загружается в веб-браузер, она анализируется и преобразу­ ется в дерево узлов и объектов просто большой объект - объектную модель, которая и есть DOM. Это JavaScript. DOM - живое представление веб-страницы, и это означает, что оно постоянно обновляется по мере взаимодействия пользователя со страницей. 1 Пересчет (reflow) - глобальный процесс, который включается при изменении размеров, положения - Прим. пер. элементов или при добавлении/удалении элементов страницы.
Виртуальный Вот пример реального <!DОСТУРЕ DOM для DOM 1 79 простой НТМL-страницы: ht111l> <htl'lt> <head> <tttte>Exa111ple Page</rttte> </head> <Ьоdу> 111у page!</h1> <p>Thi.s i.s an exa111ple paragraph.</p> <h1 class= headi.ng >Welco111e to 11 11 <Ul> <lt>Ite111 1</lt> <ti.>Ite111 2</ti.> <lt>Ite111 3</lt> </Ul> </Ьоdу> </htl'll> В этом примере реальный DOM представлен древовидной структурой, состоящей из узлов для каждого НТМL-элемента на странице. Вот так могла бы выглядеть древовидная структура, хотя для лучшего понимания она слишком упрощена. В реальном DOM на каждый узел приходится гораздо больше свойств и методов. Тем не менее это представление должно помочь нам понять, как документ моделирует­ ся в виде объекта: const do111 = { type: docu111ent doctype: ht111l chi.ldren: [ 11 11 11 11 , , { type: е le111ent tagNa111e: ht111l chi.ldren: 11 11 11 11 , , { type: ele111ent tagNa111e: head chtldren: 11 11 11 11 , , { type: е le111ent tagNa111e : ttt le chHdren: "Exa111ple Page 11 11 11 }, ], , 11 , 11 ,
80 Глава З }, { type: "еlerient" , tagNar1e: "Ьоdу", chHdren: [ { type: "elerient", tagNa111e: "h1", i.nnerНТML: "WelcOl'le to chi. ldгen: [], classNar1e: "headi.ng", Р1У page ! ", }, { type: "elerient", tagNar1e: "р", chHdгen: "Thi.s i.s an exar1ple рагаgгарh.", }, { type: "eler1ent", tagNar1e: "ul", chHdгen: [ { type: "е ler1ent" , tagNar1e: "li.", chHdгen: "Iteri 1", }, { type: "ele111ent", tagNar1e: "li.", chHdгen: "Iteri 2", }, / / ], }, ], }, ], }, ], }; ••• ВСРЮВЬ/'lе сюда все ОСf'ЮЛЬНое
Виртуальный DOM 81 Каждый узел в дереве представляет собой НТМL-элемент и содержит свойства и методы, позволяющие манипулировать им с помощью JavaScript. Например, мы можем использовать метод docul'lent.queгySelector() для извлечения определенного узла из реального // Извлечь узел DOM <h1> и изменения его содержимого: const h1Node = docul'lent.querySelector(".head1.ng"); // Изменимь его содержимое i.f (h1Node) { hlNode.tnnerHTML = "Updated Head1.ng!"; } console.log(h1Node); В этом примере мы извлекаем элемент hl класса "head1.ng", используя метод docul'lent.querySelector(). Затем мы изменяем содержимое элемента, устанавливая для его свойства 1.nnerHTML значение "Updated Head1.ng!" ("Обновленный заголовок!"). Это изменяет текст на странице с "Welcol'le to 111у page!" ("Добро пожаловать на мою страницу!") на "Updated Head1.ng!". Представленный код не кажется слишком сложным, но здесь есть несколько мо­ ментов, на которые следует обратить внимание. мы используем DOM. Этот метод Во-первых, docul'lent.querySelector() для извлечения элемента из реального принимает СSS-селектор в качестве аргумента и возвращает первый элемент, соот­ ветствующий селектору. В данном случае мы передаем селектор класса который соответствует элементу .head1.ng, hl класса "head1.ng". Здесь есть небольшая опасность, потому что, хотя метод docu111ent.querySelector яв­ ляется мощным инструментом для выбора элементов в реальном селекторов CSS, DOM на основе при работе с большими и сложными документами одной из потен­ циальных проблем с производительностью этого метода является то, что он может быть медленным. Метод должен стартовать в верхней части документа и спускать­ ся вниз, чтобы найти нужный элемент, а это может занять много времени. Когда мы вызываем docu111ent.querySelector() с помощью СSS-селектора, браузеру приходится искать соответствующие элементы по всему дереву документа. Это оз­ начает, что поиск может быть медленным, особенно если документ большой и име­ ет сложную структуру. Кроме того, браузер должен сам проанализировать селек­ тор, что может быть непростым процессом и зависеть от сложности селектора. Напротив, docul'lent. getEle111entByid не требует последовательного просмотра доку­ мента, как селекторы CSS, ожидается, что атрибуты и обладает повышенной специфичностью, поскольку 1.d являются уникальными, поэтому такой подход, как правило, более эффективен.
82 Глава 3 С точки зрения сложности во время выполнения при помощи нотации Big d, getEleмentByid в современных браузерах часто приблизительно обозначается как (0(1)), учитывая, что браузеры, вероятно, используют механизмы хеширования, такие как хеш-таблицы, для эффективного сопоставления ID ele111ent. Хотя в (0(1)), важно учи­ --+ идеале поиск по хеш-таблице в среднем выполняется на уровне тывать, что наихудшие сценарии, такие как коллизии хешей, могут привести к затя­ гиванию поиска. Учитывая, что браузеры на самом деле не обеспечивают 100%- ную уникальность идентификатора, такие коллизии хешей могут быть более чем вероятными. Впрочем, благодаря расширенным функциям хеширования и стратегиям масшта­ бирования в современных браузерах, такие случаи редки. ~ Для тех из нас, кто не учился в компьютерной школе и, возможно, не пони­ мает нотацию Big О, поясним, что это удобный инструмент, используемый разработчиками для оценки того, насколько быстро или медленно будет выполняться фрагмент кода. Особенно это важно по мере увеличения объ­ ема данных, с которыми код должен работать. По сути, обозначение Big О дает высокоуровневое понимание алгоритмов как с точки зрения времен­ ной сложности (как время выполнения увеличивается с увеличением раз­ мера входных данных), так и с точки зрения пространственной сложности (как объем используемой памяти возрастает с увеличением размера вход­ ных данных). Это часто выражается с помощью таких терминов, как (0(1)), (О(п)), (О(п • п)), или О(п 2 )), где п - это размер входных данных. Поэтому, когда разработчики говорят об "эффективности" или "масштаби­ руемости" кода, они часто ссылаются на значения ритмам с меньшей временной и Big пространственной О, стремясь к алго­ сложностью. Это стремление является гарантией того, что их программное обеспечение со­ хранит производительность, даже если оно будет обрабатывать все больше и больше данных. Кроме того, поскольку идентификаторы должны быть уникальными, они не очень хорошо подходят для использования на странице с несколькими повторно исполь­ зуемыми компонентами. В этом и заключается преимущество querySelector, по­ скольку его можно использовать, например, для выбора нескольких элементов с одинаковым именем класса. Тем не менее querySelector, который может принимать широкий спектр СSS­ селекторов, имеет переменную сложность. В худшем случае, когда методу, воз­ можно, потребуется просмотреть весь DOM, чтобы убедиться в совпадении или его отсутствии, его сложность может составлять (О(п)), где п в DOM. - количество элементов Однако фактическое время выполнения может быть меньше (О(п)) для бо­ лее специфических селекторов или если совпадение найдено в дереве 2 DOM на ран- Нотация Big О прилумана для описания алгоритмической сложности. Она призвана показать, как сильно возрастает количество ·операций при увеличении размера данных. - При.,11. пер.
Виртуальный DOM 83 ней стадии. Кроме того, еще существуют дополнительные вычислительные затраты на анализ и проверку самих селекторов. Стоит отметить, что разница в производительности между docuмent.getEleмentByid и docuмent.querySelector может быть незначительной в небольших документах или при поиске элементов в определенных областях дерева документа. Однако в боль­ ших и более сложных документах разница может стать более заметной. Кто-то скажет, что весь этот аргумент об "эффективности процессора" преувеличен и беспокоиться об этом не стоит. Хотя это может быть справедливо, а может и не быть, никто не станет сомневаться в дополнительной ценности, которую предос­ тавляет виртуальный DOM от React, поскольку он позволяет компоновать логику и не беспокоиться об управлении состоянием в такой нестабильной среде, как Мы говорим, что DOM DOM. изменчив, потому что на него влияет множество факторов, включая взаимодействие с пользователем, сетевые запросы, клиентские скрипты и события, которые могут привести к его изменению в любое время. туальный DOM, React, через вир­ защищает нас от этих вредных факторов, используя виртуальный DOM. Мы углубляемся в эти тонкие детали, потому что для того, чтобы по-настоящему свободно работать с работа с DOM- React, важно понимать общую сложность непростая задача, но с React DOM. Грамотная у нас есть выбор: либо самим пере­ мещаться по этому минному полю и время от времени наступать на мины, либо ис­ пользовать инструмент, который помогает нам безопасно перемещаться по помощью виртуального DOM с DOM. Хотя мы обсудили здесь некоторые небольшие нюансы выбора элементов, у нас не было возможности глубже разобраться в подводных камнях работы с DOM напря­ мую. Давайте сделаем это сейчас, чтобы полностью понять ценность, которую пре­ доставляет виртуальный DOM от React. Подводные камни реального В реальном DOM DOM есть несколько ловушек, которые могут затруднить создание вы­ сокопроизводительного веб-приложения. Некоторые из этих затруднений включа­ ют проблемы с производительностью, кроссбраузерной совместимостью и проре­ хами в привести системе к безопасности, уязвимостям, когда связанным прямое с манипулирование межсайтовым DOM может (cross-site скриптингом scripting, XSS). Производительность Одной из самых больших проблем, связанных с реальным изводительность. Всякий раз, когда в DOM DOM, является его про­ вносятся изменения, такие как добавле­ ние или удаление элемента, изменение текста или атрибутов элемента, браузер должен пересчитать макет и перерисовать затронутые части страницы. Это может быть медленным и ресурсоемким процессом, особенно для больших и сложных веб-страниц.
Глава 84 3 Как упоминалось ранее, чтение свойства offsetWi.dth элемента DOM кажется про­ стой операцией, но на самом деле она может привести к дорогостоящему пересчету макета страницы. Это связано с тем, что offsetWi.dth - это вычисляемое свойство, которое зависит от расположения элемента и его предков, а это означает, что брау­ зеру необходимо убедиться в актуальности информации о расположении, прежде чем он сможет вернуть точное значение. В худшем случае чтение свойства offsetWi.dth элемента в нотации Big О будет оце­ нено как (О(п)). Это связано с тем, что доступ к данному свойству потенциально может вызвать пересчет страницы, который включает в себя пересчет позиций ма­ кета для ряда элементов. В данном контексте (п) представляет количество элемен­ тов DOM, затронутых пересчетом. Несмотря на то что прямой доступ к свойствам осуществляется быстро, связанные с этим побочные эффекты, такие как пересчет, могут привести к увеличению затрат и уменьшению производительности пропор­ ционально количеству элементов на странице. Если вы хотите избежать потенциального переформатирования, вызванного досту­ пом к свойствам элементов макета, таким как offsetWi.dth, мы можем использовать определенные методы, чтобы повысить производительность операции. Вот один из подходов, использующий метод getBoundi.ngCli.entRect(), который позволяет выпол­ нять пакетные чтение и запись макета (батчинг чтения и записи): // Более эффекf'IUВНЫй дocf'lyn к свойсf'lвам макеf'lа functi.on getOffsetWi.dthWi.thoutTri.ggeri.ngReflow(ele111ent) { let wi.dth; // Все операции чf'/ения одним пакеf'lом const rect = ele111ent.getBoundi.ngCli.entRect(); wi.dth = rect.wi.dth; // ... // прочие операции чf'lения Далее следуЮРI операции записи, если они ecf'lь return wi.dth; } const ele111ent = docul'1ent.querySelector(".1'1yEle111ent"); const wi.dth = getOffsetWi.dthWi.thoutTri.ggeri.ngReflow(ele111ent); console.log(wi.dth); Используя getBoundi.ngCl i.entRect( ), мы извлекаем несколько свойств макета за один вызов, что снижает вероятность запуска нескольких избыточных пересчетов out thrashing). (lay- Кроме того, с помощью пакетных чтения и записи, мы можем еще больше свести к минимуму пересчет макета, т. е. избежать повторяющихся и не­ нужных действий, вызванных частым чередованием чтения и записи свойств эле-
Виртуальный ментов макета (рис. 3.1 ). 85 DOM Повторения могут значительно снизить производитель­ ность веб-страницы, что приведет к замедлению взаимодействия с пользователем. Благодаря стратегическому доступу к свойствам макета и пакетной обработке (бат­ чингу) наших операций, мы можем сделать наше взаимодействие в Интернете плавным и отзывчивым. [.11: [о Console Elements @ с .!. .±, 0 1000ms i Sources output.jsbln.com #1 -:-J ► Animatlon ► lnteractlons Maln - Performance Memory О Screenshots О Memory 3000 ms » ф 0 4000ms х ф 5000ms CPU .а. 1500ms 1450ms 1550ms 16.6 ms 16.7 ms 16.7 ms 16.7 ms 16.7 ms 16.6 ms 16.7 ms 16.7 ms 16.7 ms 16.7 ms 16.6 ms 16.7 ms • L.ayout Shlfts т • 2000ms 1400 ms ЗSOms Frameв 11S Network https://output.jsbln .com/aГisum/9/quiet 1 • 11 Summary Вottom-Up Call Tree Event Log ■ Layout Total Тime 28.30 ms Self Тime 5 µs Nodes Тhat Need Layout 5 of 1618 Layout root sty\e#j sbin-css Pendlng for 50.4 ms lnitiator ~ Рис. Однако даже 3.1. Избыточный пересчет макета getBoundingCli.entRect() может вызвать повторный пересчет, если у вас есть очередь из ожидающих изменений макета. Ключом к повышению производи­ тельности здесь является сведение к минимуму количества раз, когда вы заставляе­ те браузер пересчитывать макет. Когда вы это делаете, постарайтесь за один раз получить столько информации, сколько вам нужно. React выполняет все это за нас "из коробки", используя виртуальный стве промежуточного уровня между операциями с реальным DOM в каче­ DOM . Рассмотрим следующий пример, в котором у нас есть простой НТМL-документ с одним элементом <!DОСТУРЕ <hml> <head> htl'll> div:
Глава 86 3 <ti.tle>Readi.ng offsetWi.dth <style> #мy-di.v { wi.dth: 100рх; hei.ght: 100рх; background-color: red; exaмple</title> } </style> </head> <Ьоdу> <di.v i.d="мy-di.v"></di.v> <scri.pt> vаг di.v = docuмent.getEleмentByld("мy-di.v"); console.log(di.v.offsetWi.dth); </scri.pt> </Ьоdу> </htl'll> Когда мы загружаем этот документ в браузер и открываем консоль разработчика, мы видим, что свойство offsetWi.dth элемента di.v регистрируется в консоли. Однако, чего мы не видим, так это закулисной работы, которую должен выполнить браузер, чтобы вычислить значение offsetWi.dth. Для суть того чтобы Performance понять этой работы, мы можем использовать панель (Производительность) в наших инструментах разработчика для запи­ си временной шкалы действий браузера по мере загрузки и рендеринга страницы. Когда мы делаем это, мы видим, что браузер выполняет несколько операций с ма­ кетом и графикой по мере обработки документа. В частности, мы можем видеть, что есть две операции компоновки, которые соответствуют чтению offsetWi.dth в скрипте. Выполнение каждой из этих операций компоновки занимает некоторое время (в данном случае около 2 мс), даже если просто считывается значение свойства. Это связано с тем, что браузеру необходимо убедиться в актуальности информации о макете, прежде чем он сможет вернуть точное значение, для чего требуется выпол­ нить полную верстку документа. Хотя 2 миллисекунды могут показаться не таким уж большим отрывком времени, в сложном приложении это может стать проблемой. В общем, мы должны быть осторожны при чтении свойств макета, таких как offsetWi.dth, поскольку это может вызвать непредвиденные проблемы с производи­ тельностью. Если нам нужно прочитать значение таких свойств несколько раз, нам следует рассмотреть возможность кеширования значения переменной, чтобы избе­ жать ненужных пересчетов макета. В качестве альтернативы мы можем использо­ вать API requestAni.мati.onFraмe, чтобы отложить считывание свойства до следующего анимационного кадра, когда браузер уже выполнит необходимые вычисления макета.
Виртуальный DOM 1 87 Для того чтобы лучше понять неожиданно возникающие проблемы с производи­ тельностью в реальном DOM, давайте разберем несколько примеров. Рассмотрим следующий НТМL-документ: <!ООСТУРЕ ht111l> <htJll'l> <head> <ti.t'le>Exa111ple</ti.t'le> </head> <Ьоdу> <u'l id="list"> <'li.>Ite111 1</'li.> <'li.>lte111 2</'li.> <'li.>Ite111 3</ti.> </u'l> </Ьоdу> </htJll'l> Предположим, мы хотим добавить новый элемент в список с помощью JavaScript. Для этого мы могли бы написать следующий код: const list = docu111ent.getEle111entByld("list"); const newlte111 = docu111ent.createEle111ent("li"); newlte111.textContent = "Ite111 4"; list.appendChild(newlte111); Обратите внимание, что здесь мы используем getEle111entByld вместо querySelector, потому что: ♦ мы знаем идентификатор; ♦ мы осведомлены о компромиссах производительности. Давайте продолжим. Этот код выбирает элемент ul с идентификатором "list", создает новый элемент li, "Ite111 4" и добавляет его к спи­ присваивает его текстовому содержимому значение ску. Когда мы запустим этот код, браузер должен произвести перерасчет макета и перерисовать соответствующую часть страницы с новым элементом. Этот процесс может быть медленным и ресурсоемким, особенно для больших спи­ сков. Например, предположим, что у нас есть список из 1ООО элементов, и мы хо­ тим добавить новый элемент в его конец. Для этого мы могли бы написать сле­ дующий код: const list = docu111ent.getEle111entByld("list"); const newlte111 = docu111ent.createEle111ent("li"); newlte111.textContent = "Ite111 1001"; list.appendChild(newlte111);
88 Глава 3 Когда мы запускаем этот код, браузер должен пересчитать макет и перерисовать весь список, даже если был добавлен только один элемент. Это может занять зна­ чительное количество времени и ресурсов, особенно на медленных устройствах или при работе с большими списками. Для того чтобы еще раз проиллюстрировать эту проблему, рассмотрим другой пример: <!DOCTYPE ht111l> <htJtll> <head> <ti.tle>Exa111ple</ti.tle> <style> #li.st Н { backgгound-coloг: #fSfSfS; } .htghltght { backgгound-coloг: yellow; } </style> </head> <Ьоdу> <ul i.d="li.st"> <li.>Ite111 l</li.> <li.>Ite111 2</Н> <li.>Ite111 З</Н> </ul> <button oncli.ck="hi.ghli.ght()">Hi.ghli.ght Ite111 2</button> <scгi.pt> function hi.ghli.ght() { const i.te111 = docu111ent.queгySelector("#li.st li.:nth-chi.ld{2)"); i.te111.classLi.st.add("hi.ghli.ght"); } </scгi.pt> </Ьоdу> </hml> В этом примере у нас есть список из трех элементов и кнопка, которая при нажатии выделяет второй элемент. При этом нажатии браузеру приходится пересчитывать макет и перерисовывать весь список, даже если изменился только один элемент. Это может привести к заметной задержке или мерцанию пользовательского интер­ фейса, что способно разочаровать пользователей.
Виртуальный В целом проблемы с производительностью реального DOM 89 DOM могут стать для нас серьезным вызовом, особенно при работе с большими и сложными всб-страницами. Существуют методы устранения подобных проблем, такие как оптимизация селек­ торов, использование делегирования операций или использование анимации событий, CSS, пакетное чтение/запись DОМ­ однако они могут быть сложными в реализации. В результате многие разработчики для решения этих проблем обратились к вирту­ альному DOM. Именно он позволяет нам создавать пользовательские интерфейсы, которые отличаются большей эффективностью и производительностью, абстраги­ руясь от сложностей реального DOM и предлагая более простой способ представ­ ления пользовательского интерфейса. Но ... действительно ли так необходима экономия нескольких миллисекунд? Что ж, производительность центрального процессора (ЦП) - критический фактор, кото­ рый может сильно повлиять на успех приложения. В современную цифровую 1по­ ху, когда пользователи ожидают быстрых и отзывчивых веб-сайтов, для нас, всб­ разработчиков, важно уделять приоритетное внимание эффективности исполЬ'3ова­ ния ЦП, чтобы обеспечить гладкую и бесперебойную работу наших приложений. Отличная статья под названием носят миллионы", "Milliseconds make millions" ("Миллисекунды https://oreil.ly/BtXCh) в блоге веб-разработчиков Google при­ еще больше подтверждает эти мысли. Прямые манипуляции с DOM, которые запускают переформатирования макета и перерисовку страницы с нуля, могут привести к увеличению загрузки процессора и времени обработки, что способно повлечь за собой задержки и даже сбои в работе пользователей. Это может быть особенно проблематично для владельцев мало­ мощных устройств, таких как смартфоны или планшеты, которые могут иметь ОI'­ раниченную вычислительную мощность и небольшой объем памяти. Во мно1,их частях мира пользователи могут получать доступ к нашим веб-приложениям на старых или менее функциональных устройствах, что может еще больше усугубить проблему. Уделяя приоритетное внимание производительности процессора, мы можем созда­ вать приложения, доступные пользователям на самых разных устройствах, незави­ симо от их вычислительной мощности или объема памяти. Это может привести к увеличению вовлеченности, повышению коэффициента конверсии и в конечном счете к более успешному присутствию в Интернете. Концепция виртуального DOM от React позволила создавать веб-приложс1111я с возможностью более эффективного использования ЦП; применение его алгоритмов рендеринга может помочь сократить время обработки и повысить общую произво­ дительность. Кроссбраузерная совместимость Еще одна проблема, связанная с реальным DOM, - это кроссбраузерная совмести­ мость. Различные браузеры по-разному моделируют документы, что может привес­ ти к несогласованностям и ошибкам в веб-приложениях. Такая ситуация была бо-
Глава 90 3 лее распространенной в то время, когда был выпущен React, и гораздо реже встре­ чается сейчас. Тем не менее эта проблема может затруднить разработчикам созда­ ние веб-приложений, которые легко работают в разных браузерах и на различных платформах. Одной из основных проблем, связанных с кроссбраузерной совместимостью, явля­ ется то, что некоторые элементы и атрибуты браузерами. В результате разработчикам DOM могут поддерживаться не всеми приходилось тратить дополнительное время и усилия на внедрение обходных путей и резервных версий, чтобы обеспе­ чить корректную работу приложений на всех целевых платформах. Именно это решает Syntheti.cEvent - React с помощью своей системы синтетических событий. это оболочка для нативных событий браузеров, предназначенная для обеспечения совместимости различных браузеров. Она устраняет несоответст­ вия между браузерами, используя следующие механизмы. Унифицированный интерфейс. В исходном JavaScript обработка событий браузера может быть сложной из-за несовместимости. Например, доступ к свойствам события может различаться в разных браузерах. Некоторые могут использовать другие используют event.srcEleP1E!nt. Syntheti.cEvent event. target, в то время как абстрагирует эти различия, предоставляя согласованный способ взаимодействия с событиями и гарантируя, что разработчикам не придется писать код для конкретного браузера: // Без React разрабомчикам npu11J11ocь бы проверямь свойсмва, // специфичные для браузеров const targetEleP1E!nt = event.target 11 event.srcEleP1E!nt; // С React, благодаря SyntheticEvent, functton handleClick(event) { const target = event.target; // ... осмальной код эмого не мребуемся } Объединяя собственные события браузеров в синтетическую систему событий Syntheti.cEvent, React защищает разработчиков от многих несоответствий и при­ чуд, характерных для систем событий браузеров. Делегирование событий. Вместо того чтобы подключать слушателей непосредственно к элементам, React отслеживает события на корневом уровне. Этот подход позволяет избежать про­ блем, связанных с тем, что некоторые события могут быть недоступны для оп­ ределенных элементов в старых браузерах. Межфункциональные улучшения. Одна из областей, в которых события браузеров демонстрируют несогласован­ ность, заключается в том, как они обрабатывают определенные события в раз­ ных элементах ввода.
Виртуальный DOM 1 91 Ярким примером является событие onChange. • В исходном JavaScript поведение события onChange отличается в зависимости от типа ввода: 0 для <i.nput type="text"> событие onChange в некоторых браузерах может за­ пускаться только после того, как ввод теряет фокус, а не сразу после изме­ нения значения; 0 для <select> срабатывание может происходить всякий раз, когда выбран параметр, даже если он совпадает с предыдущим; 0 в других случаях, особенно в старых браузерах, событие onChange может срабатывать ненадежно для всех взаимодействий пользователя с опреде­ ленными элементами формы. • Система Syntheti.cEvent от этих элементов ввода. В 0 React React: нормализует поведение события onChange для событие onChange для ввода текста (<i.nput type="text">) последовательно срабатывает при каждом нажатии клавиши, обеспечивая обратную связь в режиме реального времени; 0 для <select> он надежно запускается всякий раз, когда выбирается новая опция; 0 React гарантирует, что onChange обеспечивает согласованное взаимодейст- вие и с другими элементами формы. Таким образом, React освобождает разработчиков от работы с этими встроен­ ными несоответствиями, позволяя сосредоточиться на логике своего приложе­ ния, не беспокоясь о специфических особенностях браузеров. Доступ к нативным событиям. Если разработчикам требуется оригинальное событие браузера, оно доступно через event. nati.veEvent, обеспечивающее гибкость без ущерба для преимуществ абстракции. По сути, Syntheti.cEvent предлагает стабильную систему событий, сглаживающую причуды и различия, присущие событиям в различных браузерах. Это лишь один из конкретных способов, с помощью которого DOM React использует свой виртуальный для обеспечения удобства разработки пользовательского интерфейса. До сих пор мы обсуждали, как непосредственная работа с DOM может вызвать проблемы с производительностью и кроссбраузерной совместимостью. Давайте теперь рассмотрим способ более эффективного решения таких проблем с помощью фрагментов документа - метода, который можно считать чем-то вроде нативного предшественника виртуального DOM в React. Фрагменты документа Как мы уже видели, прямое манипулирование DOM может потребовать значитель­ ных затрат производительности, особенно при внесении целого ряда изменений.
92 Глава 3 При каждом обновлении DOM браузеру может потребоваться выполнить перерас­ чет макета и перерисовать пользовательский интерфейс, что может замедлить рабо­ ту приложения. Вот тут-то и вступают в игру фрагменты документа. Фрагмент документа жащий узлы DOM. (document fragment) - это облегченный контейнер, содер­ Он действует как временная промежуточная область, где вы Как только вы можете внести несколько изменений, не затрагивая основной DOM. закончите, вы можете добавить фрагмент документа в однократно запустив DOM, пересчет макета и его перерисовку. Таким образом, фрагменты документа очень близки к виртуальному DOM в React. Поскольку фрагменты документов представляют собой облегченные контейнеры, которые позволяют нам выполнять пакетные обновления, они обеспечивают ряд преимуществ в производительности. Пакетные обновления. Вместо того DOM, чтобы изменять несколькими отдельными обновлениями весь вы можете пакетно вносить изменения во фрагмент документа. Это озна­ чает, что выполняется только один пересчет и перерисовка, независимо от того, сколько элементов или изменений вы внесли во фрагмент. Эффективность использования памяти. Когда узлы добавляются к фрагменту документа, они удаляются из своего теку­ щего родительского элемента DOM. Это может помочь оптимизировать исполь­ зование памяти, особенно при изменении порядка следования больших разделов документа. Отсутствие избыточного рендеринга. Поскольку фрагмент документа не является частью активного дерева докумен­ тов, внесенные в него изменения не влияют на текущий документ, а стили и скрипты не применяются до тех пор, пока фрагмент не будет добавлен к акту­ альному DOM. Это позволяет избежать избыточных пересчетов стилей и выпол­ нения ненужных скриптов. Рассмотрим сценарий, в котором вам необходимо добавить в список несколько но­ вых элементов: const fragмent = docuмent.createDocuмentFragмent(); fог (let i = 0; i < 100; i++) { const Н = docuмent.createEleмent("H"); li.textContent = 'Iteм ${i + 1}'; fragмent.appendChild(li); } docuмent.getEleмentByld("мylist").appendChHd(fragмent); В этом примере 100 элементов списка сначала добавляются к фрагменту докумен­ та. Только после добавления всех элементов фрагмент добавляется к основному списку. Это приводит к однократному обновлению обновлений. DOM вместо 100 отдельных
Виртуальный Таким образом, DOM, фрагменты документа позволяют эффективно DOM 1 93 манипулировать объединяя несколько изменений в пакет, что сокращает количество дорого­ стоящих пересчетов и перерисовок. Для разработчиков, стремящихся к оптималь­ ной производительности своих веб-приложений, использование фрагментов доку­ ментов может привести к более плавному взаимодействию с пользователем и сокращению времени рендеринга. Виртуальный DOM в React логично сравнить с усовершенствованной реализацией концепции фрагмента документа. Вот как это можно кратко описать. Пакетные обновления. Подобно фрагментам документов, виртуальный DOM в React объединяет не­ сколько изменений в пакет. Вместо прямого изменения реального ждом изменении состояния или свойства, нения в виртуальном DOM при ка­ React сначала компилирует эти изме­ "diffs") между текущим виртуальным DOM. Эффективное определение различий. Затем DOM React определяет различия (или и реальным DOM. Этот процесс гарантирует, что в реальный DOM будут внесены только необходимые изменения, аналогично тому, как фрагменты до­ кумента сокращают прямые манипуляции с DOM. Единственный рендеринг. Как только различия идентифицированы, DOM обновляется за один раз, что очень похоже на добавление полностью заполненного фрагмента документа. Это сводит к минимуму дорогостоящие повторные пересчеты и перерисовки. По сути, в то время как фрагменты документа предлагают способ сгруппировать и оптимизировать набор изменений перед обновлением реального ный DOM от React DOM, виртуаль­ делает еще один шаг вперед, разумно распределяя обновления по всему пользовательскому интерфейсу приложения, обеспечивая максимальную эффективность рендеринга. Более того, React превращает все эти фрагменты доку­ ментов в детали реализации, о которых нам, обычным разработчикам, не нужно беспокоиться, что позволяет сосредоточиться на более важных аспектах создания наших продуктов. Давайте теперь в деталях рассмотрим, как работает виртуальный DOM. Как работает виртуальный DOM Виртуальный реальному DOM - это подход, который помогает избежать ошибок, присущих DOM. Создавая виртуальное представление DOM в памяти, можно вно­ сить изменения в виртуальное представление без непосредственного изменения реального DOM, аналогично использованию фрагментов документа. Это позволяет фреймворку или библиотеке обновлять реальный DOM более эффективным и про­ изводительным способом, не заставляя браузер выполнять какую-либо работу по повторному вычислению макета страницы и перерисовке элементов.
94 Глава Виртуальный 3 DOM также помогает улучшить процесс разработки элементов и ме­ тоды их обновления, предоставляя согласованный API, который устраняет разли­ чия между разными браузерными реализациями реального Например, если DOM. docurтient. appendChi. ld отличается в другой среде выполнения, это не имеет значения при использовании JSX и виртуального DOM. Это упрощает разработчикам созда­ ние веб-приложений, которые легко работают в разных браузерах и на различных платформах. React использует виртуальный DOM для создания пользовательских интерфейсов. В этом разделе мы рассмотрим, как работает реализация виртуального DOM в React. Rеасt-элементы В React пользовательские интерфейсы представлены в виде дерева Rеасt-элемен­ тов, которые являются упрощенными представлениями компонентов или НТМL­ элементов. Они создаются с помощью функции React.createElefllent и могут быть вложенными для создания сложных пользовательских интерфейсов. Вот пример элемента const elerтient React: = React.createElerтient( "di..v", { classNarтie: "rтiy-class" }, "Hello, world!" ); Этот код создает Rеасt-элемент, который представляет элемент класса rтiy-class и текстовым содержимым Отсюда мы можем console. log(elerтient). увидеть <di.v> с именем если применим "Hello, world! ". фактически созданный элемент, Код выглядит следующим образом: { $$typeof: Syrтibol(react.elerтient), type: "di.v", key: null, геf: null, props: { classNarтie: "rтiy-class", chi.ldren: "Hello, world!" }, _owner: null, _store: {} } Это представление элемента ные блоки приложения React, React. Rеасt-элементы- это наименьшие строитель­ которые описывают то, что должно отображаться на экране. Каждый элемент представляет собой простой объект JavaScript, который
Виртуальный описывает компонент (элемент HTML) DOM 1 95 вместе с любыми соответствующими свой­ ствами или атрибутами. Rеасt-элемент, показанный в блоке кода, представлен в виде объекта с несколькими свойствами: $$typeof Такая запись используется допустимым $$typeof React Rеасt-элементом. для подтверждения того, что объект является В данном случае это Syмbol(react.eleмent). может иметь другие значения в зависимости от типа элемента. Syмbol(react.fragмent) Когда элемент представляет фрагмент React. Syмbol(react.portal) Когда элемент представляет портал React. SyмЬol(react.proftler) Когда элемент представляет профилировщик React. Syмbol(react.provider) Когда элемент представляет поставщика контекста Как правило, $$typeof React. служит в качестве маркера типа, который определяет тип Rеасt-элемента. Мы рассмотрим некоторые из них более подробно позже в этой книге. type Это свойство определяет тип компонента, который представляет элемент. В дан­ ном случае это "dtv", что указывает на то, что это элемент мый "хает-компонентом". Свойство type элемента React <dtv> DOM, называе­ может быть либо стро­ кой, либо функцией (или классом, но мы не говорим об этом, потому что от этого постепенно отказываются). Если это строка, то она представляет собой "dtv", "span", "button" и т. д. Если это функция, то она представляет пользовательский компонент React, который, по сути, является просто функцией JavaScript, возвращающей JSX. имя НТМL-тега, например Вот пример элемента с пользовательским типом компонента: const MyCoмponent = (ргорs) => { return <dtv>{props.text}</dtv>; }; const мyEleмent = <МyCOP1ponent text="Hello, world!" />; В данном случае типом свойства мyEleмent является MyCoмponent, который пред­ ставляет собой функцию, определяющую пользовательский компонент. Значе­ ние мyEleмent как объекта Rеасt-элемента будет равно: { $$typeof: Syмbol(гeact.eleмent), type: MyCoмponent,
Глава 96 3 key: null, ref: null, props: { text: "Hello, world!" }, _owner: null, _store: {} } Обратите внимание, что значение type задается для функции MyCoмponent, которая является типом компонента, представляемого элементом, а props содержит так называемые пропсы 3 (props), передаваемые компоненту, в данном случае { text: "Hello, world!" }. Когда React обнаруживает элемент-функцию, он вызывает эту функцию с про­ псами элемента, а возвращаемое значение будет использоваться в качестве до­ di.v. Вот как осуществляется ренде­ React: React погружается глубже и глубже в черних элементов, в данном случае элемента ринг пользовательских компонентов структуру элементов, пока не достигаются скалярные значения, которые затем отображаются в виде текстовых узлов, или, если достигнуто значение undefi.ned, null либо ничего не отображается. Вот пример элемента со строковым типом: const ~yEleмent = <di.v>Нello, world!</di.v>; В данном случае свойством type элемента ~yEleмent является представляет собой строку с именем НТМL-тега. Когда который "di.v", React обнаруживает элемент со строковым типом, он создает соответствующий НТМL-элемент с та­ ким именем тега и отображает его дочерние элементы внутри этого элемента. ref Это свойство позволяет родительскому компоненту запрашивать ссылку на ба­ зовый узел DOM. Обычно оно используется тогда, когда необходимо прямое управление DOM. В этом случае значение ref равно null. props Это свойство представляет собой объект, содержащий все атрибуты и пропсы, которые были переданы компоненту. В данном случае у него есть два свойства: classNaмe и chi.ldren. classNaмe определяет имя класса элемента, а chi.ldren пред­ ставляет собой содержимое элемента. _owner Это свойство, доступное только в непроизводственных сборках ется React React, использу­ для отслеживания компонента, создавшего этот элемент. Эта инфор­ мация используется для определения того, какой компонент должен отвечать за обновление данного элемента при изменении его свойств или состояния. 3 Пропсы (от англ. props)- произвольные входные данные, которые принимают компоненты и возвращают Rеасt-элементы, описывающие то, что мы хотим увидеть на экране. - Прим. пер.
Виртуальный Вот пример, демонстрирующий, как используется свойство DOM 1 97 _owner: functi.on Parent() { return <Chttd />; } functi.on Chi.ld() { const elePJent = <dtv>Нello, world!</dtv>; console.log(elePJent._owner); // Рагепt return eleмent; } Chi.ld создает Rеасt-элемент, представляющий эле­ world!". Свойство _owner этого элемента присваива­ ется родительскому компоненту Parent, который является компонентом, создав­ шим компонент Chi. ld. В этом примере компонент мент React <di.v> с текстом "Нello, использует эту информацию, чтобы определить, какой компонент должен отвечать за обновление элемента при изменении его свойств или состояния. В этом случае, если родительский компонент получает новые свойства, Chi.ld React Parent обновляет свое состояние или соответственно обновит дочерний компонент и связанный с ним элемент. Важно отметить, что свойство React и .store _owner является внутренней деталью реализации не должно использоваться в коде приложения . Свойство _store Rеасt-элемента - это объект, который используется React для внутреннего хранения дополнительных данных об элементе. Конкретные свой­ ства и значения, хранящиеся в _store, не являются частью общедоступного не должны быть доступны напрямую. Вот пример того, как может выглядеть свойство _store: { vali.dati.on: nutt, key: nutt, ori.gi.nalProps: { classNaмe: 'мy-class', chi.ldren: 'Нello, world!' }, ргорs: { classNaмe: 'мy-class', chi.ldren: 'Нello, world!' }, _self: nutt, _source: { fi.leNaмe: 'MyCoмponent.js', li.neNuмЬer: 10 }, _owner: { _currentEleмent: [Ci.rcular], _debugID: 0, stateNode: [MyCoмponent] }, _i.sStati.c: fatse, _warnedAboutRefsinRender: fatse, } API и
Глава 98 3 Как вы можете видеть, _store включает в себя различные свойства, такие как vaHdati.on, key, oгi.gi..nalProps, ргорs, _self, _source, _owner, i..sStati..c и _warnedAboutRefsinRender. Эти свойства используются React для отслеживания различных аспектов состояния и контекста элемента. Например, свойство _source в режиме разработки применяется для отслеживания имени файла и номера строки, в которых был создан элемент, что может быть полезно при отладке. Свойство _owner используется для отслеживания компо­ нента, создавшего ori..gi..nalProps - элемент, как обсуждалось ранее. А свойства ргорs и для хранения пропсов, переданных компоненту. Опять же, важно отметить, что свойство _store является внутренней деталью реализации React и не должно быть доступно непосредственно в коде приложе­ ния, и именно по этой причине мы не будем углубляться в данную тему. Сравнение виртуального и реального DOM Функция React.createEleмent и встроенный в DOM DOM метод createEleмent схожи в том, что они оба создают новые элементы; однако React. createEle111ent создает элементы React, а docu111ent.createEle111ent создает узлы DOM. Они сильно отличаются по своей реализации, но концептуально схожи. React. createEleмent - это функция, предоставляемая React, которая создает новый виртуальный элемент в памяти, тогда как docu111ent. cгeateEle111ent - доставляемый не будет DOM API, это метод, пре­ который создает новый элемент также в памяти, пока он присоединен к с DOM помощью функций API, таких как docu111ent. appendChi.. ld или аналогичных. Обе функции принимают имя тега в качестве первого аргумента, в то время как React.createEle111ent принимает еще и дополни­ тельные аргументы для указания пропсов и дочерних элементов. Например, давайте сравним, как бы мы создали элемент <di..v>, используя оба метода: // Используем React.cгeateEleмent const di..vEle111ent = React.createEle111ent( "dt.v", { classNaмe: "111y-class" }, "Hello, World!" ); // Используем DОМ API cгeateEleмent const di..vEle111ent = docuмent.createEle111ent("di..v"); di..vEle111ent.classNa111e = "111y-class"; di..vEle111ent.textContent = "Hello, World!"; Виртуальный DOM в React схож по концепции с реальным DOM в том смысле, что оба представляют собой древовидную структуру элементов. Когда компонент отрисован, React создает новое виртуальное дерево дущим виртуальным деревом DOM DOM, React сравнивает его с преды­ и вычисляет минимальное количество измене­ ний, необходимых для обновления старого дерева в соответствии с новым. Это на-
Виртуальный зывается процессом согласования работать в компоненте (reconciliation). 1 99 DOM Вот пример того, как это может React: functi.on Арр() { const [count, setCount] = useState(0); гetuгn ( <di.V> <h1>Count: {count}</h1> <Ьutton onC1tck={() => setCount(count </di.V> ); + l)}>Increfllent</Ьutton> } Для наглядности этот компонент также можно представить следующим образом: functi.on Арр() { const [count, setCount] = React.useState(0); гetuгn React.createE1efllent( "dtv", null, React.createE1efllent("h1", null, "Count: ", count), React.createElefllent( "button", { onCltck: () => setCount(count + 1) }, "Increfllent" ); } В вызовах компо­ нента не тре­ createElefllent первым аргументом является имя НТМL-тега или React, вторым аргументом - объект свойств (или null, если свойства буются), а любые дополнительные аргументы представляют собой дочерние эле­ менты. При первом рендеринге компонента React создает виртуальное дерево DOM сле­ дующим образом: dtv Г ~ "Count: L button L 0" "Increl'IE!nt" При нажатии на кнопку React глядит следующим образом: dtv Г ~ "Count: L button L 1" "Increl'IE!nt" создает новое виртуальное дерево DOM, которое вы­
Глава 100 Затем вычисляет, что необходимо обновить только текстовое содержимое React элемента h1, 3 и обновляет лишь эту часть реального DOM. React дает возможность эффективно обнов­ лять реальный DOM, а также позволяет React беспрепятственно работать с другими библиотеками, которые напрямую управляют DOM. Использование виртуального DOM в Эффективные обновления При изменении состояния компонента во элементов React, React или пропсов React создает новое дере­ представляющее обновленный пользовательский интерфейс. Затем это новое дерево сравнивается с предыдущим деревом, чтобы определить минимальный набор изменений, необходимых для обновления реального DOM. Для этого используется алгоритм сравнения. Этот алгоритм сравнивает новое дерево элементов React с предыдущим деревом и определяет различия между ними. Это рекурсивное сравнение. Если узел изменил­ ся, React обновляет соответствующий узел в реальном лен или удален, React DOM. Если узел был добав­ добавляет или удаляет соответствующий узел в реальном DOM. Алгоритм сравнивает новое дерево со старым узел за узлом, чтобы выяснить, какие части дерева изменились. Алгоритм сравнения React сильно оптимизирован и нацелен на минимизацию ко­ личества изменений, которые необходимо внести в реальный Алгоритм ра­ DOM. ботает следующим образом. ♦ Если узлы на корневом уровне двух деревьев различны, React заменит все дере­ во новым. ♦ Если узлы на корневом уровне совпадают, React обновит атрибуты узла, если они изменились. ♦ Если дочерние элементы узла различны, менты, которые изменились. React React обновит только те дочерние эле­ не создает заново все поддерево; он обновля­ ет только те узлы, которые изменились. ♦ Если дочерние элементы узла те же, но их порядок изменился, порядок узлов в реальном DOM, ♦ Если узел удален из дерева, React изменит фактически не создавая их заново. React удалит его ♦ Если в дерево был добавлен новый узел, ♦ Если тип узла изменился (например, с из реального React добавит его DOM. в реальный DOM. dtv на span), React удалит старый узел и создаст новый узел нового типа. ♦ Если у узла есть свойство key, React использует его, чтобы узнать, следует ли заменить узел или нет. Это может быть полезно, когда вам нужно сбросить со­ стояние компонентов.
Виртуальный Алгоритм сравнения React эффективен и позволяет DOM 101 React обновлять реальный DOM быстро и с минимальными изменениями. Это помогает повысить производитель­ ность приложений React и упрощает создание сложных, динамичных пользователь­ ских интерфейсов. Излишний повторный рендеринг Хотя алгоритм сравнения React действительно играет решающую роль в эффектив­ DOM за счет минимизации необходимых изменений, ном обновлении реального существует общая проблема, с которой могут столкнуться разработчики: ненужный повторный рендеринг. Так устроен React: когда состояние компонента изменяется, React повторно ото­ бражает этот компонент и всех его потомков. Под повторным отображением (рен­ дерингом) мы подразумеваем, что React вызывает каждый функциональный компо­ нент рекурсивно, передавая каждому компоненту его пропсы в качестве аргумента. React не пропускает компоненты, пропсы которых не изменились, но вызывает все функциональные компоненты, которые являются дочерними по отношению к роди­ тельскому, состояние или про псы которого изменяются. Это связано с тем, что React не знает, какие компоненты зависят от состояния компонента, который изме­ нился, поэтому ему приходится повторно отображать их все, чтобы обеспечить со­ гласованность пользовательского интерфейса. Это может привести к серьезным проблемам с производительностью, особенно при работе с большими и сложными пользовательскими интерфейсами. Например, Chi. ldCoмponent когда в следующем фрагменте будет повторно отображаться каждый раз, изменяется состояние ParentCoмponent, даже если пропсы, Chi. ldCoмponent, не изменяются: 'U'lport React, { useState} frOPI "геасt"; const Chi.ldCoмponent = ({ мessage }) => { return <di.v>{мessage}</di.v>; }; const ParentCoмponent = () => { const [count, setCount] = useState(0); return ( <d'\.v> <button onCli.ck={() => setCount(count + l)}>Increмent</Ьutton> <Chi.ldCoмponent мessage="Thi.s i.s а stati.c мessage" /> </d'\.v> ); }; ехрогt default ParentCoмponent; переданные в
Глава 102 3 В этом примере: ♦ ParentCoмponent имеет переменную состояния count, которая увеличивается при каждом нажатии кнопки; ♦ Chi. ldCoмponent получает статический пропс мessage. Поскольку этот пропс не ме­ няется, в идеале мы бы не хотели, чтобы Chi. ldCoмponent повторно отображался каждый раз, когда ParentCoмponent изменяется; ♦ однако из-за поведения React по умолчанию, Chi.ldCoмponent будет повторно ото­ бражаться всякий раз, когда заново отображается ParentCoмponent, что происхо­ дит при каждом изменении его состояния; ♦ это неэффективно, поскольку Chi. ldCoмponent не зависит от состояния count ком­ понента ParentCoмponent; ♦ поскольку пропс и состояние Chi. ldCoмponent не изменились, новый рендеринг был бессмысленным: предположительно, он вернул тот же результат, что и в прошлый раз, так что это было напрасной тратой усилий. Это проблема, которую нам часто приходится оптимизировать, особенно в боль­ ших приложениях, где многие компоненты могут перерисовываться без необходи­ мости, что приводит к потенциальным проблемам с производительностью. Решение данной задачи требует продуманного подхода к управлению повторным рендерин­ гом компонентов, гарантирующего, что изменения в состоянии или пропсах на бо­ лее высоком уровне иерархии компонентов не приведут к массовым ненужным по­ вторным рендерингам компонентов-потомков. Благодаря продуманному структурированию компонентов и разумному использованию функций оптимиза­ ции React, таких как мемо и useMeмo, разработчики могут лучше управлять повтор­ ными рендерингами и поддерживать высокую производительность приложений. Более подробно мы рассмотрим это в главе 5. Обзор главы В этой главе мы рассмотрели различия между реальным и виртуальным веб-разработке, а также преимущества использования последнего в Сначала мы поговорили о реальном DOM DOM в React. и его ограничениях, таких как большое время рендеринга и проблемы с кроссбраузерной совместимостью, которые могут затруднить разработчикам создание веб-приложений, без проблем работающих в разных браузерах и платформах. Для того чтобы проиллюстрировать это, мы рас­ смотрели, как создать простую веб-страницу с использованием API реальных DOM и как эти АРI-интерфейсы могут быстро стать громоздкими и сложными в управ­ лении по мере увеличения сложности страницы. Затем мы углубились в изучение виртуального гие ограничения реального ный DOM DOM. DOM и того, как он устраняет мно­ Мы исследовали, как React использует виртуаль­ для повышения производительности за счет минимизации количества обновлений, необходимых для реального DOM, что важно с точки зрения времени
Виртуальный рендеринга. Мы также рассмотрели, как React текущей и предыдущей версий виртуального ного способа обновления реального 103 DOM использует элементы для сравнения DOM и расчета наиболее эффектив­ DOM. Для того чтобы проиллюстрировать преимущества виртуального DOM, мы рас­ смотрели, как создать такую же простую веб-страницу с использованием компо­ нентов React. Мы сравнили этот подход с использованием реального React компактнее ли, что компоненты DOM и увиде­ и проще в использовании несмотря на то, что сложность страницы возросла. Мы также рассмотрели различия между React. createEler1ent и docul'IE!nt. createElel'IE!nt и увидели, как мы можем создавать компоненты с помощью JSX, который предос­ тавляет аналогичный НТМL-синтаксис, что упрощает понимание структуры вирту­ ального DOM. Наконец, мы выяснили, как алгоритм сравнения React может приводить к излиш­ ним повторным рендерингам, которые могут серьезно снизить производительность, особенно при работе с большими и сложными пользовательскими интерфейсами. В главе 5 мы рассмотрим, как можно оптимизировать рендеринг с помощью Rеасt­ функций l'IE!fl10 и useМef'ю. В целом мы узнали о преимуществах применения виртуального ботке и о том, как React DOM в веб-разра­ использует эту концепцию для упрощения и повышения эффективности создания веб-приложений. Проверьте ваши знания Найдите минутку, чтобы ответить на следующие вопросы: 1. Что такое 2. Что такое фрагменты документа, чем они похожи и чем отличаются от вирту­ ального DOM DOM в и как он соотносится с виртуальным DOM? React? 3. Какие проблемы могут возникнуть при использовании 4. Как виртуальный DOM DOM? обеспечивает более быстрый способ обновления пользо­ вательского интерфейса? 5. Как работает рендеринг React? Какие потенциальные проблемы здесь могут воз­ никнуть? Что дальше? В главе 4 мы подробно рассмотрим согласование React и его FiЬеr-архитектуру.

ГЛАВА 4 Внутри согласования Для того чтобы по-настоящему свободно пользоваться React, нам нужно понимать, что конкретно выполняют его функции. До сих пор мы разбирались с React.createEleмent. Мы также достаточно подробно изучили виртуальный JSX и DOM. DOM в Давайте теперь сосредоточимся на практическом применении виртуального React и поймем, каково назначение ReactDOМ.createRoot(eleмent). render(). В частно­ сти, мы разберемся, как реальный DOM React создает свой виртуальный DOM, а затем обновляет с помощью процесса, называемого согласованием. Разбираемся в процессе согласования DOM в React - это схема желаемого состояния React использует эту схему и с помощью процесса, (reconciliation), реализует ее в конкретной хост-среде, в Если коротко, то виртуальный пользовательского интерфейса. называемого согласованием качестве которой обычно выступает веб-браузер, но, возможно, и другие среды, такие как платформы iOS и Android, и многие другие. Рассмотрим следующий фрагмент кода: 'U'll)Ort { useState} frOl'I "react"; const Арр = () => { const [count, setCount] = useState(0); return ( <1'1a'\.n> <d'\.V> world!</h1> <span>Count: {count}</span> <button onCl'\.ck={() => setCount(count </d'\.V> </1'1a'\.n> <h1>Нello, ); }; + 1)}>Increмent</button>
Глава 106 4 Этот фрагмент кода содержит декларативное описание того, каким мы хотим ви­ деть состояние нашего пользовательского интерфейса, а именно описание дерева элементов. И наши коллеги по команде, и React могут прочитать это и понять, что мы пытаемся создать приложение-счетчик с кнопкой, нажатие на которую увели­ чивает значение счетчика. Для того чтобы понять, что такое согласование, давайте разберемся, что делает React внутри приложения, когда сталкивается с подобным компонентом. Сначала главе 3. JSX представляет нам дерево элементов React. С ним мы познакомились в При вызове компонент Арр возвращает элемент ми которого являются другие элементы React, дочерними элемента­ React. Элементы React неизменяемы (для нас) и представляют желаемое состояние пользовательского интерфейса. Они не представляют фактического состояния пользовательского интерфейса. Элементы React создаются с помощью React.cгeateEleP1ent или символа< JSX, поэтому это бу­ дет транспилировано в следующий код: const Арр = О => { const [count, setCount] = useState(0); return React.cгeateEleP1ent( "мai.n", nutl, React.cгeateEleP1ent( 11 d"iv 11 , nutl, React.cгeateEleP1ent("h1", nul.t, "Нello, woгld!"), nul.t, "Count: ", count), React.cгeateEleP1ent("span", React.cгeateEleP1ent( "button", { onCli.ck: () "IncreP1ent" => setCount(count + 1) }, ) ) ); }; Этот код должен дать нам дерево созданных элементов примерно так: { type: ргорs: "мai.n", { { type: "di.v", chi.ldгen: React, которое выглядит
Внутри согласования 1 107 props: { chi.ldгen: { type: 11 hl 11 , { ргорs: chi.ldren: 11 woгld! Hello, 11 , }, }, { type: 11 span 11 , { ргорs: chi.ldгen: [ Count: 11 11 count], }, }, { type: ргорs: 11 button 11 , { onCli.ck: () => setCount(count chi.ldren: 11 Increмent 11 , }, + 1), }, ]' }, }, }, } Данный фрагмент представляет собой виртуальный нашего компонента Counter. DOM, который образован из Поскольку это первый рендеринг, это дерево теперь привязано к браузеру с использованием минимального числа вызовов функций им­ перативного программного интерфейса DOM (DOM API). Как React обеспечивает DOM? Это достигается пу­ одно обновление реального DOM, т. е. ми­ минимальное количество обращений к АРI-интерфейсу тем объединения обновлений нимального воздействия на vDOM в DOM по причинам, рассмотренным в предыдущих гла­ вах. Давайте разберемся в этом более подробно, чтобы полностью понять процесс пакетной обработки (батчинг). Пакетная обработка В главе 3 мы обсуждали фрагменты документов в браузерах как часть встроенных АРI-интерфейсов коллекции узлов DOM. Фрагменты - это DOM, которые действуют облегченные контейнеры, содержащие как временная промежуточная область,
Глава 108 4 где вы можете вносить множество изменений, не затрагивая основной (реальный) DOM. Далее вы, наконец, добавляете фрагмент документа в DOM, запуская новый процесс однократного пересчета и перерисовки страницы. Аналогичным образом, React пакетно обновляет реальный вания, объединяя несколько обновлений кращает количество модификаций vDOM в одно реального DOM и, DOM во время согласо­ обновление DOM. Это со­ следовательно, повышает производительность веб-приложений. Для того чтобы понять это, давайте рассмотрим компонент, который быстро обнов­ ляет свое состояние несколько раз подряд: functi.on Exa~ple() { const [count, setCount] = useState(0); const handleCli.ck = () setCount((prevCount) setCount((prevCount) setCount((prevCount) => => => => { prevCount + 1); prevCount + 1); prevCount + 1); }; return ( <di.V> <p>Count: {count}</p> <Ьutton onCli.ck={handleCli.ck}>Incre~nt</button> </di.V> ); } В этом примере функция ной обработки React handleCli.ck вызывает обновил бы реальный setCount три раза подряд. DOM подряд трижды, тогда Без пакет­ как значе­ ние count изменилось бы лишь один раз. Это бьmо бы расточительно и медленно. Однако, поскольку React выполняет пакетные обновления, он вносит одно обнов­ ление в DOM с count + 3 вместо трех обновлений с count + 1 каждый раз. Для того чтобы рассчитать наиболее эффективное пакетное обновление DOM, React создаст новое дерево vDOM как ответвление текущего дерева vDOM с об­ новленными значениями, где count равно 3. Это новое дерево будет нужно согласо­ вать с тем, что в данный момент находится в браузере, фактически превратив 0 в 3. Затем React вычислит, что для новое значение 3 для vDOM DOM требуется всего одно обновление, используя вместо того, чтобы обновлять DOM три раза. Вот как пакетная обработка вписывается в общую картину, и это часть более широкой те­ мы, в которую мы собираемся углубиться: процесс согласования следующего ожи­ даемого состояния DOM с текущим DOM. Прежде чем мы разберемся, как работает React сегодня, давайте рассмотрим, как React выполнял согласование с помощью устаревшего средства согласования "stack", которое использовалось до версии 16. Это поможет нам лучше понять не-
Внутри согласования обходимость в популярном сегодня средстве согласования FiЬеr-согласователем ~ Fiber, 109 называемом еще (Fiber reconciler). На данном этапе стоит отметить, что все темы, которые мы собираемся обсудить, - это детали реализации React, которые могут и, скорее всего, React от React. Цель состоит в том, чтобы, механизмах React, мы лучше поняли, как эф­ будут меняться со временем. Здесь мы отделяем механизм работы реального практического использования разобравшись во внутренних фективно использовать React в приложениях. Небольшой экскурс в историю Ранее в React для рендеринга использовалась стековая структура данных. Для того чтобы убедиться, что мы с вами на одной волне, давайте вкратце обсудим стековую структуру данных. Стековое согласование (наследие) В информатике стек - это линейная структура данных, которая соответствует принципу "последним пришел первым вышел" - (last in first out, LIFO). Это озна­ чает, что последний элемент, добавленный в стек, будет удален первым. Стек имеет две фундаментальные операции - push и рор, которые позволяют, соответственно, добавлять и удалять элементы с вершины стека. Стек можно представить как набор элементов, расположенных вертикально (стоп­ кой), причем самый верхний элемент является последним добавленным элементом. Вот иллюстрация стека в формате ASCII с тремя элементами: 1з 1 1_1 1 2 1 1 1 I_I 1 1_1 В этом примере последним добавленным элементом является элемент находится на вершине стека. Элемент 3, который 1, который был добавлен первым, находится на дне стека. В этом стеке операция это может быть на­ push добавляет элемент в верхнюю часть стека. В коде выполнено на JavaScript с использованием массива и метода push, пример, следующим образом: const stack = [); stack.push(l); // stack.push(2); // stack.push(З); // смек сейчас выгляди~ как смек сейчас выгляди~ как смек сейчас выгляди~ как [1] [1, 2] [1, 2, З]
Глава 110 4 Операция рор удаляет верхний элемент из стека. В коде это может быть реализова­ но на JavaScript с использованием массива и метода рор, например, так: const stack = [1, 2, З]; const top = stack.pop(); // top меперь равен З, а смек В этом примере метод рор удаляет верхний элемент значение. Массив - [1, 2] (3) из стека и возвращает его stack теперь содержит оставшиеся элементы (1 и 2). Первоначальным средством согласования (согласователем} React был алгоритм, основанный на стеке, который использовался для сравнения старых и новых вирту­ альных деревьев и последующего обновления DOM. Хотя стековое согласование хорошо работало в простых случаях, оно создавало ряд проблем по мере роста раз­ мера и сложности приложений. Давайте кратко рассмотрим, почему это имело место. Для этого мы разберем при­ мер, содержащий схему обновлений, которые необходимо произвести: 1. Несущественный компонент, требующий больших вычислительных затрат, потребляет ресурсы процессора для выполнения рендеринга. 2. Пользователь вводит данные в элемент '\.nput. Button становится активной, если ввод корректен. 4. Компонент, содержащий форму Forl'I, сохраняет состояние, 3. Кнопка ся повторный рендеринг. В коде мы бы выразили это следующим образом: i.Plport React, { useReducer} frOl'I "геасt"; const '\.n'\.t'\.a1.State = { text: "", '\.sVattd: fa\se }; funct1.on FOГPI() { const [state, d'\.spatch] = useReducer(reducer, '\.n'\.t'\.a1.State); const hand1.eChange = (е) => { d'\.spatch({ type: "hand1.einput", pay1.oad: e.target.va1.ue }); }; return ( <di.V> <Expensi.veCOP1pOМnt /> <i.nput va1.ue={state.text} onChange={hand1.eChange} /> <Button dtsaЫed={!state.'\.sVa1.'\.d}>SuЬr1it</Button> </di.V> ); } при этом выполняет­
Внутри согласования functton reducer(state, actton) { switch (actton.type) { case "handlelnput": return { text: actton.payload, tsValtd: actton.payload.length > 111 0, }; defautt: throw new Еггог(); } } В этом случае стековый согласователь будет отображать обновления последова­ тельно, не имея возможности приостанавливать или откладывать работу. Если до­ рогостоящий с точки зрения вычислений компонент блокирует отображение поль­ зовательского ввода, то ввод будет воспроизводиться на экране с заметной задержкой. Это приводит к ухудшению взаимодействия с пользователем, поскольку он видит, что текстовое поле не отвечает на запросы. Вместо этого было бы гораздо приятнее иметь возможность распознавать вводимые пользователем данные как обновление с более высоким приоритетом, чем рендеринг второстепенного компо­ нента, и обновлять экран, чтобы отразить вводимые данные, при этом откладывая рендеринг второстепенного и дорогостоящего с точки зрения вычислений компо­ нента. Необходимо иметь возможность прекратить текущую работу по рендерингу, если она прерывается рендерингом с более высоким приоритетом, например вводом данных пользователем. Для этого React должен иметь представление о приоритете определенных типов операций рендеринга над прочими операциями рендеринга. Стековый согласователь не определял приоритетность обновлений, что означало, что менее важные обновления могли блокировать более важные обновления. На­ пример, обновление всплывающей подсказки с низким приоритетом может блоки­ ровать обновление ввода текста с высоким приоритетом. Обновления виртуального дерева выполнялись в том порядке, в каком они были получены. В приложении React обновления виртуального дерева могут иметь разный уровень важности. Например, обновление данных, введенных в форму, может быть более важным, чем обновление индикатора, показывающего количество лайков записи, поскольку пользователь напрямую взаимодействует с данными, вводимыми в поля формы, и ожидает, что форма будет реагировать адекватно. В случае стекового согласования обновления выполнялись в том порядке, в кото­ ром они были получены, что означало, что менее важные обновления могли блоки­ ровать более важные. Например, если обновление счетчика лайков было получено
Глава 112 4 до обновления формы ввода, обновление счетчика было бы выполнено первым и могло бы заблокировать обновление формы. Если обновление счетчика лайков требует много времени (например, из-за выпол­ нения дорогостоящих по затратам вычислений), это может привести к заметной задержке или сбою в работе пользовательского интерфейса, особенно если пользо­ ватель взаимодействует с приложением во время обновления. Еще одна проблема, связанная со стековым согласованием, заключалась в том, что оно не позволяло прерывать или отменять обновления. Это означает, что даже если бы стековый согласователь имел представление о приоритете обновления, не было никаких гарантий, что он мог бы хорошо работать с различными приоритетами, избавляясь от второстепенной работы, если было запланировано высокоприоритет­ ное обновление. В любом веб-приложении не все обновления одинаково важны: случайное появ­ ление неожиданного уведомления не так значимо, как реакция на мое нажатие кнопки, потому что последнее является преднамеренным действием, требующим немедленной реакции, в то время как первое даже не ожидается и может быть не­ желательным. В стековом согласовании обновления нельзя было прервать или отменить, что оз­ начало, что излишние обновления, такие как отображение всплывающего окна, иногда производились в ущерб взаимодействию с пользователем. Это может при­ вести к выполнению ненужной работы с виртуальным деревом и что нега­ DOM, тивно скажется на производительности всего приложения. По мере роста размера и сложности приложений метод стекового согласования сталкивался с рядом проблем. В основном они были связаны с нехваткой времени и медленным реагированием пользовательских интерфейсов. Для того чтобы решить эти проблемы, команда согласователь React разработала новый инструмент под названием FiЬеr­ (Fiber reconciler), который основан на другой структуре данных, на­ зываемой FiЬеr-деревом. Давайте рассмотрим эту структуру данных в следующем разделе. FiЬеr-согласователь FiЬеr-согласователь использует другую структуру данных, называемую торая (fibers) представляет собой единицу создаются из элементов React, работы для FiЬеr-согласования. "Fiber", ко­ "Волокна" которые мы рассматрив~и в главе 3, с клю­ чевым отличием в том, что они поддерживают свое состояние и долговечны, в то время как элементы React недолговечны ("эфемерны") и не имеют состояния. Марк Эриксон описывает (Mark Erikson), разработчик Redux и известный эксперт по React, Fibers как "внутреннюю структуру данных React, которая представляет фактическое дерево компонентов в определенный момент времени". Действитель­ но, это хороший подход к Fibers, и он является фирменным для Марка, который на
Внутри согласования 113 момент написания этой книги работал над решениями обратной отладки 1 (time- travel debugging) приложений React в компании Replay. Эта группа разработчиков создает инструмент, который позволяет вам перематывать назад и воспроизводить состояние вашего приложения в любой момент его работы для отладки. Если вы еще не знакомы с этой технологией, посетите Replay.io (https://www.replay.io/) для получения дополнительной информации. Подобно тому, как представляет собой дерево элементов, vDOM React использует Fiber-дepeвo для согласования, которое, как следует из названия, представляет со­ бой дерево "волокон", непосредственно смоделированное по образцу vDOM. Fiber как структура данных Структура данных в Fiber React является ключевым компонентом FiЬеr-согла­ сователя. FiЬеr-согласователь позволяет устанавливать приоритеты обновлений и выполнять их одновременно, реагирования приложений что повышает производительность и оперативность Давайте рассмотрим структуру данных React. Fiber бо­ лее подробно. По своей сути, структура данных Fiber представляет собой представление экземп­ ляра компонента и его состояния в приложении тура данных Fiber React. Как уже говорилось, струк­ спроектирована как изменяемая структура и может обновляться и перестраиваться по мере необходимости в процессе согласования. Каждый экземпляр узла содержит информацию о компоненте, который он Fiber представляет, включая его пропсы, состояние и дочерние компоненты. Fiber-yзeл также содержит информацию о своем положении в дереве компонентов, а также метаданные, которые используются FiЬеr-согласователем для определения приори­ тетов и выполнения обновлений. Вот пример простого Fiber-yзлa: { tag: 3, // З = ClassCo~ponent type: Арр, key: null, геf: null, ргорs: { nal'le: "Tejas", age: 30 }, stateNode: Applnstance, 1 Time travel debugging (TTD) - решение для обратной отладки, которое позволяет записывать выполнение кода в приложении или процессе и воспроизводить его как вперед, так и назад. TTD улучшает отладку, т. к. вы можете вернуться назад во времени, чтобы лучше понять условия, которые приводят к конкретной ошибке. Кроме того, вы можете воспроизвести ее несколько раз, чтобы по­ нять, как лучше исправить проблему. - Пр1ш. пер.
Глава 114 4 retuгn: FtЬerParent, chtld: FtberChtld, stЫtng: FtberStЫtng, tndex: 0, // ... } В этом примере у нас есть Fiber-yзeл, который представляет ClassCol'1ponent с име­ нем Арр. Fiber-yзeл содержит следующую информацию о компоненте. tag В данном случае это значение 3, которое React использует для идентификации компонентов класса. Каждый тип компонента (компоненты класса, функцио­ нальные компоненты, границы ожидания и ошибки, фрагменты и т. д.) имеет собственный числовой идентификатор. type Имеет значение Арр и относится к функции или компоненту класса, который представляет Fiber-yзeл. props ( {naf"1e: "Tejas", age: 30}) представляют входные пропсы компонента или вход- ные аргументы функции. • stateNode Экземпляр компонента Арр, который представляет Fiber-yзeл. Его положение в дереве компонентов: return, chHd, stbН.ng и i.ndex - дает FiЬеr­ согласователю возможность "пройтись по дереву", идентифицируя родителей, потомков, сиблингов и индекс Fiber-yзлa. FiЬеr-согласование предполагает сравнение текущего Fiber-дepeвa со следующим FiЬеr-деревом, чтобы выяснить, какие узлы должны быть обновлены, добавлены или удалены. В процессе согласования FiЬеr-согласователь создает Fiber-yзeл для каждого эле­ мента React в виртуальном DOM. Для этого существует функция под названием createFtberFrof"1TypeAndProps. Конечно, другой способ задать "type и props" звать их в элементе React. разом: { type: "dtv", props: { classNaf"1e: "contatner" } } Как мы помним, элемент React это на­ выглядит следующим об­
Внутри согласования Эта функция возвращает Fiber-yзeл, полученный из элементов React. 1 115 Как только FiЬеr-узлы созданы, FiЬеr-согласователь использует рабочий ЦUЮ/ для обновления пользовательского интерфейса. Рабочий цикл начинается с корневого Fiber-yзлa и продвигается вниз по дереву компонентов, помечая узел как "dirty" ("грязный"), если он нуждается в обновлении. Как только цикл достигает конца, он возвращает­ ся по дереву обратно, создавая в памяти новое дерево DOM, независимое от среды браузера, которое в конечном итоге будет выведено (сброшено) на экран. Этот процесс представлен двумя функциями: Ьeg'\.nWoгk перемещается вниз, помечая ком­ поненты как "требующие обновления", и C01'1pleteWoгk перемещается обратно вверх, создавая дерево элементов реального DOM, отделенных от браузера. Этот закадро­ вый процесс рендеринга может быть прерван и сброшен в любой момент, посколь­ ку пользователь его не видит. FiЬеr-архитектура основана на концепции, которая в мире компьютерных игр назы­ вается "двойная буферизация", когда следующий экран подготавливается за ка­ дром, а затем "сбрасывается" на текущий экран. Для того чтобы лучше понять FiЬеr-архитектуру, давайте разберемся с этой концепцией более подробно, прежде чем двигаться дальше. Двойная буферизация Двойная буферизация - это метод, используемый в компьютерной графике и об­ работке видео для уменьшения мерцания и улучшения восприятия. Этот метод предполагает создание двух буферов (или областей памяти) для хранения изобра­ жений или кадров и переключение между ними через регулярные промежутки вре­ мени для отображения конечного изображения или видео. Вот как работает двойная буферизация на практике: 1. Первый буфер заполняется исходным изображением или кадром. 2. Пока отображается первый буфер, во второй буфер добавляются новые данные или изображения. 3. Когда второй буфер готов, он подключается вместо первого буфера, и его со­ держание отображается на экране. 4. Процесс продолжается, при этом первый и второй буферы переключаются через равные промежутки времени для отображения конечного изображения или видео. Используя двойную буферизацию, можно уменьшить мерцание и другие визуаль­ ные искажения, поскольку конечное изображение или видео выводится на экран без прерываний или задержек. FiЬеr-согласование аналогично двойной буферизации, так что при обновлении те­ кущее Fiber-дepeвo разветвляется и обновляется, чтобы отразить новое состояние данного пользовательского интерфейса. Это называется рендерингом. Затем, когда альтернативное дерево будет готово и будет точно отображать состояние, которое пользователь ожидает увидеть, оно меняется местами с текущим деревом анало-
Глава4 116 гично тому, как меняются местами видеобуферы при двойной буферизации. Это называется этапом согласования или фиксацией. Благодаря использованию дерева work-in-progress2 FiЬеr-согласователь обладает рядом преимуществ: ♦ позволяет избежать внесения ненужных обновлений в реальный DOM, что мо­ жет повысить производительность и уменьшить мерцание; ♦ позволяет вычислять новое состояние пользовательского интерфейса за преде­ лами экрана и отбрасывать его, если требуется сделать новое обновление с более высоким приоритетом; ♦ поскольку согласование происходит за кадром, оно может быть приостановлено и возобновлено без искажения того, что пользователь видит в данный момент на экране. С помощью FiЬеr-согласователя из пользовательского дерева элементов ются два дерева: одно progress) "текущее" Fiber-дepeвo, а другое - JSX созда­ (work-in- Fiber-дepeвo - для выполнения работы по внесению необходимых изменений. Давайте рассмотрим эти деревья немного подробнее. FiЬеr-согласование FiЬеr-согласование происходит в две фазы: фазу рендеринга и фазу фиксации. Этот двухфазный подход, показанный на рис. 4.1, позволяет React выполнять работу по рендерингу, которую можно прервать в любое время, прежде чем передать ее ре­ зультаты в DOM и показать пользователям новое состояние. Если подробнее, пре­ рывистый рендеринг, используемый планировщиком выполнение обратно в основной поток каждые даже на устройствах с частотой ( workloop ] Рис. 120 React, позволяет возвращать мс, что меньше, чем один кадр, кадров в секунду. Фаза рендеринга Фаза фиксации ( beginWork ] [ сомр leteWork] [ COPll'litRoot ) 4.1. Процесс работы FiЬеr-согласователя Более подробно мы рассмотрим планировщик конкурентных функций 5 React. React в главе 7 по мере изучения А сейчас давайте пройдемся по этапам согласования. Фаза рендеринга Фаза рендерuнга начинается, когда в текущем дереве происходит событие, изме­ няющее состояние. React выполняет работу по внесению изменений за кадром в альтернативном дереве, рекурсивно переходя по каждому "волокну" (каждой ветви 2 Дерево work-in-progress служит в качестве "черновика", который не виден пользователю, чтобы React мог сначала обработать все компоненты, а затем вывести их изменения на экран. - Прим. пер.
Внутри согласования 117 Fiber-дepeвa) и устанавливая флаги , сигнализирующие об ожидаемых обновлениях (рис . 4.2). Как React. мы упоминали ранее, это происходит в функции, называемой ЬeginWork внутри beginWork completeWork _____. . , ~ " " [<button>) Не//о, Рис. beginWork. 4.2. lnaement Count:O world! Порядок вызова функций в фазе рендеринга Функция beginWork отвечает за установку флагов на Fiber-yзлax, указы­ вающих, следует ли их обновлять . Она устанавливает набор флагов , а затем рекур­ сивно переходит к следующему Fiber-yзлy, проделывая то же самое, пока не дос­ тигнет нижней cOl'lpleteWork и части дерева . Когда обход завершится, мы будем вызывать двигаться по FiЬеr-узлам обратно вверх . Синтаксис функции ЬeginWork следующий : function ЬeginWork( current: Fiber I null, worklnProgress: Fiber, renderLanes: Lanes ): Fiber I null; Подробнее о co111pleteWork мы расскажем позже. А пока давайте вернемся к beginWork. Синтаксис этой функции включает в себя следующие аргументы. current Ссылка на обновляемый Fiber-yзeл в текущем дереве . Используется для опреде­ ления того, что изменилось между предыдущей и новой версиями дерева и что именно необходимо обновить. Это ссылка никогда не изменяется и используется только для сравнения. worklnProgress Обновляемый Fiber-yзeл в дереве work-in-progress. Это узел, который будет по­ мечен как "грязный", если функция обновит его и вернет . renderLanes renderLanes - это новая концепция в FiЬеr-согл асователе от React, которая заме­ няет устаревшую функцию renderExpirationП111e. Это немного сложнее, чем ста­ рая концепция renderExpirationП111e, но позволяет React лучше определять при­ оритеты обновлений и повышать эффективность процесса самого обновления. Поскольку renderExpirationП111e устарела, в этой главе мы сосредоточимся на renderLanes.
118 Глава4 По сути, это растровая маска, представляющая "полосы" рабатывается обновление. Полосы - (lanes), в которых об­ это способ категоризации обновлений на основе их приоритета и других факторов. Когда в компонент React вносятся из­ менения, ему назначается полоса рендеринга в зависимости от его приоритета и других характеристик. Чем выше приоритет изменения, тем выше назначенная полоса рендеринга. Значение renderlanes передается в функцию beg'i.nWork, чтобы гарантировать, что обновления обрабатываются в правильном порядке. Обновления, назначенные полосам с более высоким приоритетом, обрабатываются раньше обновлений, назначенных полосам с более низким приоритетом. Это гарантирует, что высо­ коприоритетные обновления, такие как обновления, влияющие на взаимодейст­ вие с пользователем или на доступность, обрабатываются в первую очередь. Помимо определения приоритетов обновлений, renderlanes также помогает React лучше управлять параллелизмом. В React "временной нарезкой" для разделения длительно выполняющихся (time slicing), используется технология, называемая обновлений на более мелкие и управляемые блоки. renderlanes играет ключевую роль в этом процессе, поскольку позволяет React определять, какие обновления следует обработать в первую очередь, а какие можно отложить на более поздний срок. После завершения этапа рендеринга вызывается функция getlanesToRetrySynchronouslyOnError, чтобы определить, были ли во время ренде­ ринга созданы какие-либо отложенные обновления. Если есть отложенные об­ новления, функция updateC0111ponent запускает новый рабочий цикл для их обра­ ботки, используя beg'i.nWork и getNextlanes для обработки обновлений и определения их приоритетности на основе их полос. В г.1аве 7, посвященной концепции конкурентности React, мы гораздо подробнее рассмотрим полосы рендеринга. А пока давайте продолжим следить за процес­ сом FiЬеr-согласования. completeWork. Функция со111р leteWork применяет обновления к находящемуся в про­ цессе обработки Fiber-yзлy и создает новое реальное дерево DOM, представляющее обновленное состояние приложения. Это дерево создается отдельно от DOM, т. е. вне поля зрения браузера. Если основной средой является браузер, это означает выполнение таких действий, как docu111ent. createEle111ent или newEle111ent. appendCh'i.ld. Имейте в виду, что это дерево элементов еще не привязано к документу в браузере: React просто создает следую­ щую версию пользовательского интерфейса за кадром. Выполнение этой работы за кадром дает ей возможность быть прерываемой: независимо от того, какое сле­ дующее состояние вычисляет React, оно еще не отображается на экране, поэтому его можно отбросить в случае, если вдруг появилось какое-либо обновление с бо­ лее высоким приоритетом. В этом весь смысл FiЬеr-согласователя. Сигнатура co111pleteWork следующая: functton co111pleteWork( current: F'i.ber I null,
Внутри согласования 1 119 workinProgress: FtЬer, renderlanes: Lanes ): FtЬег I nutl; В данном случае сигнатура совпадает с сигнатурой begtnWork. Функция cor,pleteWork тесно связана с функцией ЬegtnWork. В то время как beginWork отвечает за установку флагов о состоянии "следует обновить" на Fiber-yзлe, cor,pleteWork отвечает за построение нового дерева, которое будет передано в среду выполнения приложения . Когда cor,p leteWork достигнет вершины и построит новое дерево DOM, мы говорим, что "фаза рендеринга завершена". Теперь React перехо­ дит к фазе фиксации. Фаза фиксации Фаза фиксации (рис. отвечает за обновление актуального (реального) 4.3) учетом изменений, которые были внесены в виртуальный DOM DOM с на этапе рендерин­ DOM привязывается к среде вы­ work-in-progress заменяется текущим деревом. га. На этапе фиксации новое виртуальное дерево полнения приложения, а дерево Именно на этом этапе также запускаются все эффекты. Фаза фиксации делится на две части: фазу мутации и фазу компоновки . FIЬerRootNode Текущее дерево beginWork completeWork Не//о, <span> <button> Count:O lncrement world! ' beginWork completeWork <span> .... [<button>) Не//о, Рис. world! Count:0 lncrement 4.3. Фаза фиксации с Fi.ЬerRootNode Фаза мутации. Фаза мутации это первая часть этапа фиксации, и она отвечает за обновление фактического с учетом изменений, которые были внесены в вир- DOM
Глава 120 туальный 4 DOM. Во время этой фазы определяет обновления, которые необхо­ React димо внести, и вызывает специальную функцию, называемую c01Т1i.tМutati.onEffects. Эта функция применяет обновления, которые были внесены в FiЬеr-узлы в альтер­ нативном дереве на этапе рендеринга, и таким образом модифицирует фактический DOM. Вот пример полного псевдокода того, как comi.tMutati.onEffects: functi.on comi.tMutati.onEffects(Fi.Ьer) { swi.tch (Fi.Ьer.tag) { case HostCoмponent: { // Обновление узла DОМ новыми пропсами может быть реализована функция и/или поРlомками Ьгеаk; } case HostText: { // Обновление f'leкcf'loвoгo содержимого узла DОМ Ьгеаk; } case // // ClassCoмponent: { Вызов меf'/одов жизненного цикла, f'laкux как cof'lponentDidМount и cof'lponentDidUpdate Ьгеаk; } // ... прочие дейсf'lвuя над другими f'lunaмu узлов } } На этапе мутации React также вызывает другие специальнь1е функции, такие как c01Т1i.tUnмount и c01Т1i.tDeleti.on, для удаления узлов из Фаза компоновки. Фаза компоновки - DOM, которые больше не нужны. это вторая часть этапа фиксации, и она от­ вечает за расчет нового расположения обновленных узлов в этапа React DOM. Во время этого вызывает специальную функцию под названием сО1Т1i. tlayoutEffects. Она вычисляет новое расположение обновленных узлов в Как и coммi.tMutati.onEffects, comi.tLayoutEffects также DOM. является массивной конструк­ цией ветвления исполнения кода, которая вызывает различные функции в зависи­ мости от типа обновляемого узла. В конце этапа компоновки React успешно обновит фактический перь отражает изменения, внесенные в виртуальный DOM DOM, который те­ на этапе рендеринга. С помощью разделения этапа фиксации на две части (мутация и компоновка) может эффективно применять обновления к DOM. React Работая совместно с другими ключевыми функциями FiЬеr-согласователя, этап фиксации помогает обеспечить быструю, отзывчивую и надежную работу приложений React, даже если они стано­ вятся все более сложными и обрабатывают большие объемы данных.
Внутри согласования 121 Эффекты. На этапе фиксации процесса согласования React побочные эффекты 3 вы­ полняются в определенном порядке, в зависимости от типа эффекта. Существует несколько типов эффектов, которые могут возникать на этапе фиксации, включая перечисленные далее. Эффекты размещения. Эти эффекты возникают, когда в DOM добавляется новый компонент. Напри­ мер, если в форму добавлена новая кнопка, то для добавления кнопки в DOM будет применен эффект размещения. Эффекты обновления. Эти эффекты возникают, когда в компонент добавляются новые пропсы или со­ стояние. Например, если текст кнопки изменится, произойдет эффект обновле­ ния текста в DOM. Эффекты удаления. Эти эффекты возникают, когда компонент удаляется из DOM. Например, если кнопка удалена из формы, включится эффект удаления, который приведет к удалению кнопки из DOM. Эффекты компоновки. Эти эффекты возникают до того, как браузер начнет отрисовывать страницу, и используются для обновления макета страницы. Управление эффектами макета осуществляется с помощью хука setLayoutEffect в функциональных компонентах и метода жизненного цикла cOP1ponentDi.dUpdate в классе компонентов. В отличие от этих эффектов этапа фиксации, пассивные эффекты определяются пользователем и запускаются по расписанию после того, как браузер начнет отри­ совывать страницу. Управление пассивными эффектами осуществляется с помо­ щью хука useEffect. Пассивные эффекты полезны для выполнения действий, которые не являются кри­ тичными для первоначального рендеринга страницы Такие действия могут вклю­ чать извлечение данных из API или отслеживание аналитики. Поскольку пассивные эффекты не выполняются на этапе рендеринга, они не влияют на время, требуемое для вычисления минимального набора обновлений, необходимых для приведения пользовательского интерфейса в состояние, запланированное разработчиком. Выводим все на экран React поддерживает указатель Fi.berRootNode на вершине обоих деревьев, который указывает на одно из двух деревьев: текущее дерево Fi.ЬerRootNode - current или дерево workinProgress. это ключевая структура данных, которая отвечает за управление этапом фиксации процесса согласования. Когда в виртуальный DOM вносятся обновления, React обновляет worklnProgress, оставляя текущее дерево неизменным. Это позволяет 3 Побочные эффекты (side effects) - React дерево продол- дополнительные действия, позволяющие компоненту подклю­ чаться и синхронизироваться с внешними системами. - Пр1tм. пер.
122 Глава 4 жать рендеринг и обновление виртуального DOM, сохраняя при этом текущее со­ стояние приложения. Когда процесс рендеринга завершен, React вызывает функцию cOl'l'1i.tRoot, которая отвечает за фиксацию изменений, внесенных в дерево worklnProgress, в фактическом DOM. col'll'li.tRoot переключает указатель Fi.ЬerRootNode с текущего дерева на дерево workinProgress, превращая дерево workinProgress в новое текущее дерево. С этого момента все будущие обновления будут основываться на новом текущем дереве. Этот процесс гарантирует, что приложение остается в согласованном со­ стоянии и обновления применяются правильно и эффективно. Все это происходит в браузере мгновенно. Это и есть работа по согласованию. Обзор главы В этой главе мы рассмотрели концепцию согласования React и узнали о FiЬеr­ согласователе. Мы также узнали о Fiber-yзлax, которые обеспечивают эффектив­ ный рендеринг с возможностью прерывания в сочетании с мощным планировщи­ ком. Мы также узнали о фазе рендеринга и фазе фиксации, которые являются дву­ мя основными этапами процесса согласования. Наконец, мы узнали о Fi.ЬerRootNode, ключевой структуре данных, отвечающей за управление фазой фиксации в процес­ се согласования. Проверьте ваши знания Давайте зададим себе несколько вопросов, чтобы проверить наше понимание кон­ цепций, изложенных в этой главе: 1. Что такое согласование 2. Какова роль структуры данных 3. Зачем нам нужны два дерева? 4. Что происходит при обновлении приложения? React? Fiber? Если мы сможем ответить на эти вопросы, мы будем на верном пути к пониманию работы FiЬеr-согласователя и процесса согласования в React. Что дальше? В главе 5 мы сосредоточимся на общих вопросах по React и рассмотрим некоторые продвинутые шаблоны. Мы ответим на вопросы о том, как часто применять usеМемо и когда использовать React. lazy. Мы также рассмотрим, как с помощью useReducer и useContext управлять состоянием в приложениях До встречи! React.
ГЛАВА 5 Общие вопросы и мощные шаблоны Теперь, когда мы больше знаем о том, что происходит "под капотом" React и как он работает, давайте подробнее рассмотрим его практическое применение в процессе React. В этой главе React, которые помогут написания приложений мы рассмотрим ответы на распростра­ ненные вопросы по нам быстрее освоить и понять процес­ сы запоминания, отложенную ("ленивую") загрузку и производительность. Давайте начнем с разговора о запоминании. Запоминание с помощью Запоминание (memoization) - React.memo это метод, используемый в информатике для опти­ мизации производительности функций путем кеширования их ранее вычисленных результатов. Проще говоря, запоминание сохраняет выходные данные функции на основе ее входных данных, и если функция вызывается снова с теми же входными данными, она возвращает кешированный результат, а не пересчитывает выходные данные повторно. Это значительно сокращает время и ресурсы, необходимые для выполнения функции, особенно для функций, требующих больших вычислитель­ ных затрат или часто вызываемых. Запоминание зависит от "чистоты" функции. Чистая функция определяется как функция, предсказуемо возвращающая одинако­ вые выходные данные для заданных входных данных. Примером чистой функции является: functi.on return add(nuмl, nuм2) nuмl { + пuм2; } Эта функция добавления тов (add) всегда возвращает значение З при задании аргумен­ 1 и 2 и, следовательно, может быть благополучно сохранена в памяти. Если функция использует какой-либо побочный эффект, такой как сетевое взаимодейст­ вие, она не будет запоминаться. Рассмотрим, например: async functi.on addToNuмberOfТheDay(nuм) { const todaysNuмber = awatt fetch("https://nuмber-api..coм/today") .then((r) => r.json())
Глава 124 5 .then((data) гetuгn nuм => data.nuмber); + todaysNuмber; } При вводе значения 2 эта функция будет каждый день возвращать другой результат и, следовательно, не сможет его запомнить. Возможно, это глупый пример, но бла­ годаря ему мы в общих чертах можем понять основы запоминания. Запоминание особенно полезно при выполнении дорогостоящих вычислений или при рендеринге больших списков элементов. Рассмотрим следующую функцию: let result = null; const doHardThtng = () => { tf (result) гetuгn result; // ... здесь зal'lpal'lныe дейсf'lвия result = hardStuff; гetuгn haгdStuff; }; Однократный вызов doHardThtng может занять несколько минут, чтобы выполнить сложную задачу, но повторный, третий, четвертый или п-й раз на самом деле ниче­ го сложного не делает, а возвращает сохраненный результат. В этом суть запоми­ нания. В контексте React запоминание может быть применено к функциональным компо­ нентам с помощью компонента React. мемо. Эта функция возвращает новый компо­ нент, который повторно загружается только в том случае, если его пропсы измени­ лись. Основываясь на главе 4, в идеале, теперь мы знаем, что "повторный рендеринг" означает повторный запуск функционального компонента. Если он за­ ключен в Rеасt.мемо, то функция не вызывается повторно во время согласования, если только ее пропсы не изменились. Запоминая функциональные компоненты, мы можем предотвратить ненужные повторные рендеринги, общую производительность нашего приложения Мы знаем, что компоненты React - сования, как обсуждалось в главе что может повысить React. это функции, которые вызываются для согла­ 4. React рекурсивно вызывает функциональные компоненты с их пропсами для создания дерева vDOM, которое затем используется в качестве основы для двух согласованных FiЬеr-деревьев. Иногда рендеринг (т. е. вызов компонентной функции) может занимать много времени из-за интенсивных вычислений в функциональном компоненте или вычислений при применении к DOM с помощью эффектов размещения или обновления, как описано в главе 4. Это замедляет работу нашего приложения и затрудняет работу пользователя. Запоми­ нание - это способ избежать задержек, сохраняя результаты дорогостоящих вы­ числений и возвращая их при передаче одних и тех же входных данных функции или тех же про псов компоненту.
Общие вопросы и мощные шаблоны 125 Для того чтобы понять, почему React.мef110 важен, давайте рассмотрим распростра­ ненный сценарий, организующий список элементов, которые необходимо отобра­ зить в компоненте. Например, предположим, что у нас есть список дел Todoli.st, ко­ торый мы хотим отобразить в компоненте, например, так: functi.on Todoli.st({ todos }) { гetuгn ( <Ul> {todos.~ap((todo) => ( <li key={todo.i.d}>{todo.ti.tle}</li> ))} </ul> ); } Теперь давайте поместим этот компонент в другой компонент, который будет по­ вторно отображаться при вводе информации пользователем: functi.on Арр() { const todos = Аггау.fгм({ length: 1000000 }); const [паме, setNaмe] = useState(""); ( <div> <input value={naмe} onChange={(e) => <TodoList todos={todos} /> </div> гetuгn setNaмe(e.target.value)} /> ); } В нашем компоненте приложения Арр при каждом нажатии клавиши пользователем поле ввода ToDoLi.st Todoli.st будет рендериться повторно: функциональный компонент будет повторно запускаться со своими пропсами при каждом нажатии кла­ виши. Это может привести к проблемам с производительностью (и, вероятно, так и случится), но является ключевым моментом в работе React: когда в компоненте происходит изменение состояния, каждый функциональный компонент, начиная с этого компонента и далее по дереву, повторно отрисовывается во время согласования. Если список дел большой, а компонент часто отрисовывается заново, это может привести к появлению узкого места в производительности приложения. Один из способов оптимизировать этот компонент const Мef110i.zedTodoLi.st гetuгn <Ul> ( - запомнить его с помощью = React.мef110(function Todoli.st({ todos }) { React.~ef110:
Глава 126 5 => ( <tt key={todo.td}>{todo.tttle}</tt> ))} {todos.мap((todo) </ut> ); }); Если обернуть компонент ToDoList с помощью React.l'IE!f'IO, React отобразит компо­ нент только в том случае, если его пропсы изменились. Изменения состояния ок­ ружения не повлияют на него. Это означает, что если список дел останется преж­ ним, компонент не будет отрисовываться, повторно а вместо этого будут использоваться его кешированные выходные данные. Это может значительно сэко­ номить ресурсы и время, особенно если компонент сложный, а список дел большой. Давайте рассмотрим другой пример - у нас есть сложный компонент с нескольки­ ми вложенными компонентами, рендеринг которых требует больших затрат: functton Dashboard({ data }) { гetuгn ( <dtv> <h1>Dashboard</h1> <UseгStats user={data.user} /> <RecentActi.vtty activity={data.activity} /> <IмpoгtantНessages мessages={data.мessages} /> </dtv> ); } Если пропс данных часто меняется, рендеринг этого компонента может быть доро­ гостоящим, особенно если вложенные компоненты также сложны. Мы можем оп­ тимизировать этот компонент, используя React.l'IE!f'IO для запоминания каждого вло­ женного компонента: const МeмotzedUserStats = React.мeмo(functi.on UserStats({ user }) { // }); const MeмotzedRecentActivity = React.l'IE!f'IO(functi.on RecentActtvtty({ acttvtty, }) { // }); const MeмoizediмportantMessages мessages, = React.мeмo(functi.on IмpoгtantМessages({
Общие вопросы и мощные шаблоны 127 }) { // }); function Dashboard({ data }) { гetuгn ( <d'\.V> <h1>Dashboard</h1> <НePiotzedUseгStats user={data.user} /> activity={data.activity} /> <НePiotzedRecentActivtty <НePiotzedil'lpoгtantмessages мessages={data.мessages} /> </dtv> ); } Запоминая каждый вложенный компонент, React будет отображать только те ком­ поненты, которые изменились, а кешированные выходные данные будут использо­ ваться для тех компонентов, которые не изменились. Это может значительно повы­ сить производительность компонента Dashboard и сократить количество ненужных повторных отрисовок. Таким образом, мы можем видеть, что React.мel'10 является важным инструментом для компонентов в React. оптимизации производительности функциональных Это может быть особенно полезно для компонентов, которые требуют больших затрат на визуализацию или имеют сложную логику. Как быстро освоить React.memo Давайте вкратце рассмотрим, как работает React.мel'lo. Когда в новление, ваш компонент сравнивается с результатами React происходит об­ vDOM, полученными при предыдущем рендеринге. Если эти результаты отличаются, т. е. если его пропсы изменились, согласователь запускает эффект обновления, если элемент уже суще­ ствует в среде хоста (обычно в DOM браузера), или эффект размещения, если это не так. Если его пропсы те же, компонент вновь отрисовывается, а DOM по­ прежнему обновляется. Именно для этого нам и нужен React.мel'10: чтобы избежать ненужных повторных отрисовок, когда пропсы компонента идентичны между рендерингами. Поскольку мы можем делать это в React, возникает вопрос: сколько компонентов и как часто мы должны запоминать? Конечно, если мы запомним каждый компонент, наше приложение в целом будет работать быстрее, не так ли? Сохраненные компоненты, которые отрисовываются повторно React.мel'lo выполняет так называемое поверхностное (shallow) сравнение пропсов, чтобы определить, изменились они или нет. Проблема в том, что в нение скалярных типов довольно точное, а нескалярных - JavaScript срав­ нет. Для того чтобы
128 Глава 5 провести качественное обсуждение, давайте вкратце разберем, что такое скалярные и нескалярные типы и как они ведут себя в операциях сравнения. Скалярные (примитивные) типы Скалярные типы, также известные как примитивные типы, являются основопола­ гающими. Они представляют собой единичные, неделимые значения. В отличие от более сложных структур данных, таких как массивы и объекты, скаляры не обла­ дают свойствами или методами и неизменяемы по своей природе. Это означает, что как только скалярное значение задано, его нельзя изменить, не создав совершенно новое значение. В есть несколько типов скалярных значений, включая JavaScript числа, строки, логические значения, а также символы, большие целые числа, неоп­ ределенные значения и null. Каждый из этих типов служит уникальной цели. На­ пример, в то время как числа не требуют пояснений, символы позволяют создавать уникальные идентификаторы, а неопределенные значения и null позволяют разработ­ чикам указывать на отсутствие значения в разных контекстах. Сравнивая скалярные значения, мы часто интересуемся их фактическим содержанием или величиной. Нескалярные (ссылочные) типы Выходя за рамки простоты скаляров, мы сталкиваемся с нескалярными, или ссы­ лочными, типами. Эти типы не хранят данные, а скорее являются ссылкой или ука­ зателем на то, где данные хранятся в памяти. Это различие имеет решающее значе­ ние, поскольку оно влияет на то, как эти типы сравниваются, обрабатываются и взаимодействуют в коде. В JavaScript наиболее распространенными нескалярными типами являются объекты и массивы. Объекты позволяют нам хранить структури­ рованные данные с помощью пар "ключ - значение", в то время как массивы пре­ доставляют упорядоченные коллекции. Функции также являются ссылочными ти­ пами в JavaScript. Ключевой характеристикой нескаляров является то, что несколько ссылок могут указывать на одну и ту же ячейку памяти. Это означает, что изменение данных с помощью одной ссылки может повлиять на другие ссылки, указывающие на те же данные. Когда дело доходит до сравнения, нескалярные ти­ пы сравниваются по их ссылке на память, а не по содержимому ячейки памяти. Иногда это может привести к неожиданным результатам для тех, кто не знаком с таким нюансом. Например, два массива с одинаковым содержимым, но разными ячейками памяти будут считаться неравными при сравнении с использованием оператора строгого равенства. Рассмотрим следующий пример: // Скалярные f'lиnы "а"=== "а";// stгing; верно 3 === 3; // nиf'IЬег; верно // Нескалярные f'lunы [1, 2, 3] === [1, 2, 3]; { foo: "Ьаг"} === { foo: "Ьаг"} // аггау; неверно // object; неверно
Общие вопросы и мощные шаблоны 129 При таком сравнении массивы, объекты и другие нескалярные типы сравниваются по ссылке: например, совпадают ли ссылки на массив левой части и на массив пра­ вой части выражения в соответствии с их местоположением в памяти компьютера. Вот почему сравнение возвращает значение false. То же самое верно и для объек­ тов. При сравнении объектов мы создаем два разных объекта в памяти с левой и правой стороны выражения - конечно, они не равны, это два разных объекта, ко­ торые находятся в двух разных местах памяти! Они просто имеют одинаковое со­ держимое. Вот почему React. меl'Ю может быть сложным в использовании. Рассмотрим список функциональных компонентов, который использует массив элементов в качестве пропса и отображает их: const List = React.мe11IO(functi.on List({ iteris }) { return ( <Ul> {iteris.мap((iteм) => ( <li key={iteri}>{iteri}</li> ))} </Ul> ); }); Теперь представьте, что вы используете этот компонент в родительском компонен­ те и передаете новый экземпляр массива каждый раз, когда родительский компо­ нент выполняет рендеринг: funrnon ParentCoмponent({ allFruits }) { const [count, setCount] = React.useState(0); const favoriteFruits = allFruits.filter((fruit) => fruit.isFavorite); return ( <div> <button onClick={() => setCount(count + <List iteмs={favoriteFruits} /> </div> l)}>Increмent</Ьutton> ); } Каждый раз, когда нажимается кнопка Increмent, родительский компонент Parent- Coмponent отрисовывается заново. Несмотря на то что значения элементов, передан­ ных в List, не изменились, каждый раз создается новый экземпляр массива с эле­ [ 'apple', 'banana', 'cherry' ]. Поскольку React.l'1el'IO выполняет поверхност­ ментами ное сравнение пропсов, он увидит, что этот новый экземпляр массива отличается от массива предыдущей отрисовки, и это приведет к ненужной повторной визуализа­ ции компонента List.
Глава 130 5 Для того чтобы исправить ситуацию, мы могли бы запомнить массив с помощью useMePIO: funcnon ParentCoмponent({ allFruits }) { const [count, setCount] = React.useState(0); const favoriteFruits = React.useMePIO( () => allFruits.filter((fruit) => fruit.isFavorite), хука (] ); ( <d"iv> <button onClick={() => setCount(count + <L"ist iteмs={favoriteFruits} /> </d"iv> гetuгn 1)}>Increмent</button> ); } Теперь массив создается только один раз и сохраняет одну и ту же ссылку при по­ вторном просмотре, предотвращая ненужные повторные отрисовки компонента List. Этот пример подчеркивает важность понимания сравнений ссылок при работе с React.мePIO и нескалярными пропсами. При неосторожном использовании мы можем получить проблемы с производительностью вместо ее оптимизации. React.мePIO также часто использует другой нескалярный тип - функцию. Рассмот­ рим следующий случай: <МeriotzedAvataг naмe="Tejas" url="https://github.cOl'1/tejasq.png" onChange={() => save()} /> Пропсы паме, url и onChange имеют постоянные значения и не зависят от состояния оболочки. Однако если мы сравним пропсы, то увидим следующее: "Tejas" === "Tejas"; // 'nal'/e' "https://github.coм/tejasq.png" === <- ргор; верно "https://github.coм/tejasq.png"; (() => save()) === (() => save()); // <- 'onChange' ргор; неверно Опять же, это связано с тем, что мы сравниваем функции по ссылке. Помните, что до тех пор, пока пропсы будут отличаться, наш компонент не будет запоминаться.
Общие вопросы и мощные шаблоны Мы можем бороться с этим, используя хук useCa l lback 131 внутри родительского эле­ мента Мel'юi.zedAvatar: const Parent = ({ currentUser }) => { const onAvatarChange = useCallback( (newAvatarUrl) => { updateUserМodel({ avatarUrl: newAvatarUrl, i.d: currentUser.i.d }); }, [currentUser] ); return ( <Нerloi.zedAvatar naflle="Tejas" url="https://gi.thub.coм/tejasq.png" onChange={onAvatarChange} /> ); }; Теперь мы можем быть уверены, что onAvatarChange никогда не изменится, если только не изменится что-то из его массива зависимостей (второй аргумент), напри­ мер текущий идентификатор пользователя. Благодаря этому наше запоминание становится полным и надежным. Это рекомендуемый способ запоминания компо­ нентов, которые имеют функции в качестве пропсов. Отлично! Это означает, что наши сохраненные в памяти компоненты никогда не будут повторно отрисовываться без необходимости. Верно ли это? Не совсем. Есть еще одна вещь, о которой нам нужно знать. Это рекомендация, а не правило React использует React.fl'lef'10 в качестве подсказки своему согласователю о том, что мы не хотим, чтобы наши компоненты выполняли повторный рендеринг, если их пропсы остаются прежними. Функция просто намекает. В конечном счете, что именно делает React, решает сам React. React. fl'lerю последовательно избегает по­ вторного рендеринга, который происходит каскадно от родительского компонента, и это единственная цель данной функции. Ее действия не гарантируют, что компо­ нент никогда не будет повторно отрисован. Возвращаясь к началу книги, следует отметить, что React задуман как декларативная абстракция нашего пользователь­ ского интерфейса, в которой мы описываем, что мы хотим получить, и думывает наилучший способ, как это сделать. ханизма. React. fl'lef'10 React при­ является частью этого ме­
Глава 132 5 React. 1'1Е!1'1О не гарантирует полного предотвращения повторной отрисовки, посколь­ ку React может принять решение о повторном рендеринге сохраненного в памяти компонента по различным причинам, таким как изменения в дереве компонентов или изменения глобального состояния приложения. Для того чтобы лучше это понять, давайте взглянем на некоторые фрагменты кода из исходного кода React. Сначала мы рассмотрим реализацию React. l'IE!l'IO: functi.on l'IE!l'IO(type, сомраге) { гetuгn { $$typeof: REAG_MOO_ТYPE, type, сомраге: сомраге == undeftned? nutt сомраге, }; } В этой реализации React.l'lel'IO возвращает новый объект, представляющий запоми­ наемый компонент. Объект имеет свойство запоминаемый компонент, свойство type, $$type, которое идентифицирует его как которое ссылается на исходный компо­ нент, и свойство сомраге, которое определяет функцию сравнения, используемую для запоминания. Далее, давайте посмотрим, как React.l'lel'IO применяется в согласователе: functi.on updateМerюCoмponent( current: FtЬег I nutt, worklnProgress: FtЬег, Coмponent: any, nextProps: any, renderlanes: Lanes ): nutt I FtЬег { tf (current == nutt) { const type = Coмponent.type; tf ( tsst~pleFuncttonCoмponent(type) Сомроnеnt.сомраге = nutt && && // Sif'lpleМel'IOCOf'lponent не южеf'I определШ'lь внешние пропсы. Coмponent.defaultProps == undeftned ) { tet resolvedType = type; tf (_DEV_) { resolvedType = resolveFuncttonForНotReloadtng(type); }
Общие вопросы и мощные шаблоны 133 // Если Эf'IO npocl'loй функциональный компоненf'I без пропсов по умолчанию // и f'lолько с поверхнос,rным сравнением по умолчанию,""' обновим его // до Sil'lpleМef'loCOl'lponent, чf'/обы разреыuf'lь бысl'lрые обновления. workinProgress.tag = StмpleМef'IOCOl'lponent; workinProgress.type = resolvedType; i.f (_DEV_) { validateFunctionCOl'lponentinDev(workinProgress, type); } return updateSiмpleМerюCOl'lponent( current, workinProgress, resolvedType, nextProps, renderlanes ); } i.f (_DEV_) { const innerPropTypes = type.propTypes; i.f (tnnerPropTypes) { // Внуl'lренние пропсы компонент на данный моменf'I не оценены в cгeateElel'lent. / / М,J можем nepeмecf'IUl'lb его сюда, но он еще nOl'lpeбyef'ICя // для определения nyf'lи к "ленивому" checkPropTypes( innerPropTypes, nextProps, // Определенные пропсы коду. "ргор", getCOl'lponentNaмeFrOl'lType(type) ); } !== undefined) { = getCOl'lponentNaмeFrOl'lType(type) (Coмponent.defaultProps i.f "Unknown"; i.f (!dtdWarnAЬoutDefaultPropsOnFunctionCOl'lponent[cOl'lponentNaмe]) { console.error( "%s: Support for defaultProps wHl Ье reмoved frOl'l COl'lponents "+ "tn а future release. Use JavaScript default paraмeters instead.", const coмponentNaмe 11 coмponentNaмe ); dtdWarnAЬoutDefaultPropsOnFunctionCOl'lponent[cOl'lponentNaмe] } } } = true;
Глава 134 5 const chtld = createFtberFroмTypeAndProps( Coмponent.type, null, nextProps, nutl, worklnProgress, worklnProgress.~de, renderLanes ); chtld.ref = worklnProgress.ref; chtld.return = worklnProgress; worklnProgress.chtld = chtld; return chHd; } i.f (_DEV_) { const type = Coмponent.type; const tnnerPropTypes = type.propTypes; i.f (tnnerPropTypes) { // Внуl'lренние пропсы компонен~ на данный юменf"I не оценены в cгeateElef"lent. / / М,1 южем nepeмecf"IUl'lb его сюда, но он еще nol'lpeбyef"lcя // для определения nyf"lu к "ленивому" checkPropTypes( tnnerPropTypes, nextProps, // Определенные пропсы коду. "ргор", getCмponentNa~FroмType(type) ); } } // Всегда cyщecf"lвyef"I один дочерний элеменf"I const currentChtld = ((current.chtld: any): FtЬег); const hasScheduledUpdateOrContext = checkScheduledUpdateOrContext( current, renderLanes ); i.f (!hasScheduledUpdateOrContext) { // Эfrlo будуf"I пропсы, разрешенные в defau l tPгops, // в Оf"/Лuчие Of"I cuггent.f"lef"loizedPгops, в коl'lором пропсы const prevProps = currentChtld.~~tzedProps; // Поверхносf"lное сравнение по уюлчанию let смраге = Смроnеnt.смраге; смраге = сомраге !== null? сомраге: shallowEqual; будуl'I неопределенными.
Общие вопросы и мощные шаблоны 135 tf (cOl'lpare(prevProps, nextProps) && current.ref === workinProgress.ref) { return bailoutOnAlreadyFinishedWork(current, workinProgress, renderlanes); } } // React DevTools чul'IOIOl'I Эf'IOf'I флаг. workinProgress.flags 1= Perforrr1edWork; const newChild = createWorkinProgress(currentChild, nextProps); newChild.ref = workinProgress.ref; newChild.return = workinProgress; workinProgress.child = newChild; return newChHd; } Вот краткое описание того, что происходит: 1. Начальная проверка. Функция updateМerюCor,ponent принимает несколько параметров, включая текущие и находящиеся в состоянии work-in-progress Fibers-yзлы, компонент, новые про­ псы и полосы рендеринга (которые, как уже говорилось, указывают приоритет и тайминг обновления). Проверка (if (current === null)) определяет, является ли это первоначальным рендерингом компонента. Если значение current равно null, это значит, что компонент монтируется в первый раз. 2. Тип и быстрая оптимизация пути. Затем проверяется, является ли компонент простым функциональным компо­ нентом и подходит ли он для быстрого обновления пути проверкой флажков Cor,ponent.cor,pare и Cor,ponent.defaultProps. Если эти условия выполнены, для тега work-in-progress Fiber-yзлa устанавливается значение SiP1pleМerтюCor,ponent, указы­ вающее на более простой тип компонента, который можно обновлять более эф­ фективно. 3. Проверка в процессе разработки. В режиме разработки (_DEV_) функция выполняет дополнительные проверки, такие как проверка типов пропсов и предупреждение об устаревших функциях (например, таких как 4. defaultProps в Создаем н,овый Fiber-yзeл. Если это первичный рендеринг, CreateFiЬerFroP1TypeAndProps. Этот рендерера вый 5. функциональных компонентах). React. то Fiber новый Fiber создается с помощью представляет собой единицу работы для Он устанавливает ссылки и возвращает дочерний элемент (но­ Fiber). Обн,овлен,ие существующего Fiber. Если компонент обновляется (current !== null), в режиме разработки он выпол­ няет аналогичные проверки. Затем он проверяет, нуждается ли компонент в об­ новлении, сравнивая старые пропсы с новыми, используя поверхностное срав-
136 Глава 5 пение (shallowEqual) или пользовательскую функцию сравнения, если она преду­ смотрена. 6. Отказ от обновления. Если пропсы одинаковы, а ссылка не изменилась, обновление может быть отме­ нено с помощью bai.loutOnAlreadyFi.ni.shedWork, и это означает, что дальнейшая ра­ бота по рендерингу для данного компонента не требуется. 7. Обновление work-in-progress Fiber. Если требуется обновление, функция помечает незавершенный процесс in-progress) флагом Perforl'ledWork и создает Fiber на основе текущего дочернего элемента, новый дочерний (workwork-in-progress но с новыми пропсами. Подводя итог, можно сказать, что эта функция отвечает за определение того, нужно ли обновлять сохраненный в памяти компонент (компонент, созданный с помощью React.l'1el'IO) или он может пропустить обновление для оптимизации производитель­ ности. Он обрабатывает как начальный рендеринг, так и обновления, выполняя различные операции в зависимости от того, создается ли новый Fiber или обновля­ ется существующий. Далее перечислены части этой функции, сообщающие нам об условиях, при кото­ рых компонент React.1'1ef'IO будет или не будет делать повторный рендеринг. Предыдущий рендеринг отсутствовал (начальное монтирование). Если значение current === null, компонент монтируется впервые, и, следова­ тельно, он не может пропустить рендеринг. Для рендеринга компонента созда­ ется и возвращается новый Fiber. Простая функциональная оптимизация компонента. Если компонент является простым функциональным компонентом (без пропсов по умолчанию и без пользовательской функции сравнения), его до Si.111pleМerюCOPlponent. Это позволяет React React оптимизирует использовать быстрый путь для обновлений, поскольку он может предполагать, что компонент зависит только от своих пропсов и ни от чего больше, и достаточно поверхностного сравнения, чтобы определить, следует ли его обновлять. Функция сравнения. Если есть предыдущий рендеринг, компонент обновится только в том случае, если функция сравнения вернет значение false. Эта функция сравнения может быть пользовательской, если она указана в коде, или по умолчанию использует­ ся проверка на поверхностное равенство (shallowEqual). Если функция сравнения определит, что новые пропсы равны предыдущим пропсам, а ссылка ref такая же, компонент не будет повторно отображаться, и функция выйдет из процесса рендеринга. Пропсы по умолчанию и типы реквизитов в режиме разработки. В режиме разработки (_DEV_) есть проверки на defaultProps и propTypes. Исполь­ зование defaultProps в режиме разработки вызовет предупреждение, поскольку в
Общие вопросы и мощные шаблоны будущих версиях React 137 планируется отменить defaultProps для функциональных компонентов. Типы пропсов проверяются в целях оценки. Условия выхода. Если нет запланированных обновлений или изменений контекста (hasScheduledUpdateOrContext равен false), функция сравнения считает, что старый и новый пропсы равны, а геf не изменилась, тогда функция вернет результат bai.loutOnAlreadyFi.ni.shedWork, фактически пропустив повторный рендеринг. Однако, если есть запланированные обновления или изменения контекста, ком­ понент выполнит повторную отрисовку, даже если его про псы не изменились. Это связано с тем, что считается, что обновления контекста выходят за рамки пропсов компонента. Изменения состояния, изменения контекста и запланиро­ ванные обновления также могут вызывать повторную визуализацию. Флаг выполненной работы. Если обновление необходимо, для параметра WorklnProgress устанавливается флаг Perforf'ledWork, указывающий на то, что этот параметр выполнил работу во время текущей визуализации. Таким образом, компоненты React.l'lel'IO не будут повторно отображаться, если срав­ нение между старым и новым про псами (с использованием либо пользовательской функции сравнения, либо поверхностного сравнения по умолчанию) определяет, что пропсы равны, и обновления не запланированы из-за изменений состояния или контекста. Если определено, что пропс отличается, или если произошли изменения состояния или ко1tтекста, компонент выполнит рендеринг. Запоминание с помощью React. l'IE!l'IO useMemo - это инструменты для запоминания, но с совершенно раз­ ными целями. React.l'lel'IO запоминает весь компонент, чтобы предотвратить его по­ и хук useМel'IO вторный рендеринг. useМerю применяется для запоминания конкретных вычислений внутри компонента, чтобы избежать дорогостоящих пересчетов и сохранить согла­ сованную ссылку на результат. Давайте кратко рассмотрим useМerю. Взглянем на следующий компонент: const People = ({ unsortedPeople }) => { const [nal'IE!, setNal'IE!] = useState(""); const sortedPeople = unsortedPeople.sort((a, Ь) => b.age - a.age); / / . . . другие чacf'/u компоненf'lй }; Этот компонент потенциально может замедлить работу нашего приложения из-за выполнения операции сортировки. Временная сложность операции сортировки обычно составляет О(п logn) для средних и наихудших сценариев. Если в нашем списке, скажем, миллион пользователей, это может привести к значительным вы-
Глава 138 5 числительным затратам на каждый рендеринг. С точки зрения информатики, эф­ фективность операции сортировки в значительной степени определяется количест­ вом элементов п, следовательно, временной сложностью О(п Для оптимизации процесса мы бы могли использовали хук logn). useMePIO, чтобы избежать сортировки массива при каждом отображении, особенно если массив unsoгtedPeople не изменился. Текущая реализация компонента создает значительную проблему с производитель­ ностью. Каждый раз, когда состояние обновляется, что происходит при каждом на­ жатии клавиши, когда курсор находится внутри поля ввода, компонент выполняет повторный рендеринг. Если введено имя из 1 ООО 5 символов и наш список содержит ООО человек, компонент выполнит повторную визуализацию 5 раз. Для каж­ дого рендеринга, это приведет к сортировке списка, которая требует порядка 1 ООО ООО х log(l ООО ООО) операций из-за временной сложности сортировки. Это составляет многие миллионы операций только для ввода имени из пяти символов! К счастью, эту неэффективность можно устранить с помощью хука useМel'IO, гаран­ тирующего, что операция сортировки будет выполняться только при изменении массива unsortedPeople. Давайте немного перепишем этот фрагмент кода: const People = ({ unsoгtedPeople }) => { const [na111e, setNa111e] = useState(""); const soгtedPeople = useМePIO( / / /i,/ не изменяем оригинальный массив () => [ ... unsoгtedPeople].soгt((a, Ь) => b.age - a.age), [unsoгtedPeople] ); гetuгn ( <di.v> <di.V> Enteг уоuг na111e: {" "} <i.nput type="text" placeholder="Obi.nna Ekwuno" onChange={(e) => setNa111e(e.taгget.value)} /> </di.v> <h1>Нi., {na111e}! Неге's а li.st of people <Ul> {soгtedPeople.мap((p) => ( <ti. key={p.i.d}> {p.na111e}, age {p.age} </li.> soгted Ьу age!</h1>
Общие вопросы и мощные шаблоны 139 ))} </ut> </di.v> )j }; Вот так! Намного лучше! Мы поместили значение sortedPeople в функцию, которая была передана в качестве первого аргумента useМel'1o. Второй аргумент, передавае­ мый в useМel'10, представляет собой массив значений, который при изменении при­ водит к повторной сортировке этого массива. Поскольку массив unsortedPeople со­ держит только несортированных пользователей, он будет сортироваться один раз и лишь тогда, когда список людей изменится, а не когда кто-то вводит имя в поле ввода. Это отличный пример того, как использовать хук useMel'10, чтобы избежать ненужного повторного рендеринга. useMemo считается вредным Хотя может возникнуть соблазн обернуть все объявления переменных внутри ком­ понента с помощью useMel'1o, такой подход не всегда удачен. Хук useМel'10 особенно полезен для запоминания дорогостоящих вычислительных операций или поддер­ жания стабильных ссылок на объекты и массивы. Для скалярных значений, таких как строки, числа или логические значения, использование обычно не тре­ useMel'10 буется. Это связано с тем, что скалярные значения передаются и сравниваются в JavaScript по их фактическому значению, а не по ссылке. Таким образом, каждый раз, когда вы устанавливаете или сравниваете скалярное значение - это фактиче­ ское значение, с которым вы работаете, а не ссылка на ячейку памяти, которая мо­ жет измениться. В таких случаях загрузка и выполнение функции useMel'10 могут оказаться более до­ рогостоящими, чем сама операция, которую она пытается оптимизировать. Рас­ смотрим следующий пример: const MyCooponent = () => { const [count, setCount] = useState(0); const douЫedCount = useMel'10(() => count * 2, [count]); return ( <d'iv> <p>Count: {count}</p> <p>DouЫed count: {douЫedCount}</p> <button onClick={() => setCount((oldCount) => oldCount + 1)}> Increl'1ent </button> </d'iv> ); };
Глава 140 5 В этом примере переменная douЫeCount запоминается с помощью хука useMeмo. Од­ нако, поскольку count является скалярным значением, запоминать его не обязатель­ но. Вместо этого мы можем просто вычислить удвоенное значение count непосред­ ственно в JSX: const MyC01'1ponent = () => { const [count, setCount] = useState(0); return ( <di.V> <p>Count: {count}</p> <p>DouЫed count: {count * 2}</р> <Ьutton onCltck={() => setCount((oldCount) => oldCount + 1)}> Increмent </Ьutton> </di.V> ); }; Теперь douЫedCount больше не запоминается, но компонент по-прежнему выполняет те же вычисления с меньшим потреблением памяти и накладными расходами, по­ скольку мы не импортируем и не вызываем usеМемо. Это хороший пример того, как избежать использования useМeflю, когда в этом нет необходимости. Однако может возникнуть проблема с производительностью, ведь мы заново создаем обработчик onCltck для кнопки при каждом рендеринге, поскольку он передается по ссьшке в памяти. Но действительно ли это проблема? Давайте посмотрим поближе. Некоторые предполагают, что мы должны запомнить обработчик useCa l lback: const MyC01'1ponent = () => { const [count, setCount] = useState(0); const douЫedCount = usеМемо(() => count * 2, [count]); const tncreмent = useCallback( () => setCount((oldCount) => oldCount + 1), [setCount] ); return ( <di.V> <p>Count: {count}</p> <p>DouЫed count: {douЫedCount}</p> <Ьutton onCltck={tncre111ent}>Increмent</Ьutton> </di.V> ); }; onCHck, используя
Общие вопросы и мощные шаблоны 141 А стоит ли? Ответ отрицательный. Нет смысла запоминать функцию инкремента, поскольку является встроенным элементом браузера, а не функциональ­ <button> ным компонентом React, который может быть вызван. Кроме того, под ним нет других компонентов, которые Более того, в i.nput React React продолжал бы отображать. встроенные или "хостовые" компоненты (такие как di.v, button, и т. д.) обрабатываются немного иначе, чем пользовательские компоненты, когда дело доходит до пропсов, включая пропсы функций. Вот что происходит с функциональными пропсами встроенных компонентов. Прямая передача данных. Когда вы передаете пропс функции (например, обработчик компоненту, React onCHck) встроенному передает его непосредственно фактическому элементу DOM. Это действие не создает никаких оболочек и не выполняет никакой дополни­ тельной работы с функциями. Однако в случае onCli.ck и пропсов, основанных на событиях, React использует делегирование обработки событий, а не прямое привязывание обработчиков со­ бытий к элементам onCli.ck DOM. Это означает, что когда вы предоставляете обработчик встроенному элементу обработчик onCHck React, такому как <button>, React не подключает непосредственно к DОМ-узлу кнопки. Вместо этого React прослушивает все события на верхнем уровне, используя одного слушателя со­ бытий. Он подключен к корневому каталогу документа (или корневому каталогу приложения React), и для перехвата событий, которые происходят из отдельных элементов, он использует функцию "всплывания событий". Такой подход эф­ фективен, поскольку сокращает объем памяти и время начальной настройки об­ работчиков событий. Вместо того чтобы подключать отдельные обработчики для каждого экземпляра события в каждом элементе и управлять ими, React мо­ жет обрабатывать все события определенного типа (например, нажатия на эле­ менты) с помощью единственного слушателя событий. Когда происходит собы­ тие, React сопоставляет его с соответствующим компонентом и вызывает определенные вами обработчики таким образом, чтобы они соответствовали ожидаемому пути распространения. Таким образом, даже если события отсле­ живаются на верхнем уровне, они будут вести себя так, как если бы они были привязаны непосредственно к определенным элементам. Эта система делегиро­ вания событий в основном прозрачна, когда вы пишете приложение определяете обработчики мую. Однако, по сути, onCli.ck React; вы так же, как если бы они подключались напря­ React оптимизирует обработку событий для вас. Поведение при повторном рендеринге. Встроенные компоненты из-за изменений в пропсах функций не отрисовывают­ ся повторно, если только они не являются частью более высокого компонента, который выполняет повторный рендеринг. Например, если родительский ком­ понент повторно отрисовывается и предоставляет новую функцию в качестве пропса для встроенного компонента, встроенный компонент будет выполнять рендеринг, поскольку его пропсы изменились. Однако этот повторный ренде-
Глава 142 5 ринг, как правило, выполняется быстро и обычно не требует оптимизации, если только профилирование 1 не показывает, что это проблема. Нет сравнения виртуального Сравнение виртуального DOM для DOM функций. для встроенных компонентов основано на иден­ тификации пропса функции. Если вы передадите встроенную функцию (напри­ мер, onCН.ck={O => doSOl'lethi.ng()}), это будет новая функция при каждом ренде­ ринге компонента, но React не выполняет глубокого сравнения функций для обнаружения изменений. Новая функция просто заменяет старую в элементе DOM, и таким образом мы получаем экономию производительности за счет встроенных компонентов. Объединение событий в пул. Для обработчиков событий React использует объединение событий, чтобы уменьшить нагрузку на память. Объект event, который передается в ваши обра­ ботчики событий, является синтетическим событием, которое объединяется в пул, что означает, что он повторно используется для различных событий, чтобы уменьшить затраты на сборку мусора. Это сильно отличается от пользовательских компонентов. Для пользовательских компонентов, если вы передаете новую функцию в качестве пропса, дочерний ком­ понент может выполнить повторный рендеринг, если это чистый компонент или если к нему применено запоминание (например, с помощью React.l'lel'IO), он обнаруживает изменение в пропсах. Но для хает-компонентов React поскольку не предос­ тавляет такой встроенной функции запоминания, поскольку это добавило бы на­ кладных расходов, что в большинстве случаев невыгодно. В реальных элементах DOM, которые выдает на выходе React, нет концепции запоминания; они просто обновляются с помощью ссылки на новую функцию при изменении свойств. На практике это означает, что, хотя вам следует быть осторожным при передаче новых экземпляров функций пользовательским компонентам, повторный рендеринг которых может оказаться дорогостоящим, выполнение этого со встроенными ком­ понентами не вызывает особых проблем. Однако всегда полезно помнить о том, как часто вы создаете новые функции, поскольку создание ненужных функций может привести к сбою сборки мусора, что ведет к снижению производительности в сце­ нариях с очень высокой частотой обновлений. Таким образом, usecattback здесь совсем не помогает и на самом деле может быть вреден: он увеличивает нагрузку на наше приложение. Это связано с тем, что useCattback должен быть импортирован, вызван и передан зависимостям, а затем он должен сравнить зависимости, чтобы понять, следует ли повторно вычислять функцию. Все это сопряжено со сложностями во время выполнения, которые могут скорее навредить нашему приложению, чем помочь ему. 1 Профилирование - сбор характеристик работы программы, таких как время выполнения отдельных фрагментов (обычно подпрограмм), число верно предсказанных условных переходов, число кеш­ промахов и т. д. Инструмент, используемый для анализа работы, называют профилировщиком или профайлером (profiler). Обычно выполняется совместно с оптимизацией программы. - Прим. пер.
Общие вопросы и мощные шаблоны Какой же тогда хороший пример использования функции useCa l lback useCallback? 143 Функция особенно полезна, когда у вас есть компонент, который, вероятно, часто отрисовывается, и вы передаете обратный вызов дочернему компоненту, особенно если этот дочерний shouldCol'lponentUpdate. компонент оптимизирован с помощью React.l'lel'IO или Запоминание обратного вызова гарантирует, что дочерний компонент не будет повторно отрисовываться без необходимости при отображении родительского компонента. Вот пример правильного использования useCallback: \Jllport React, { useState, useCallback} frOPI "геасt"; const ExpenstveCOl'lponent = React.l'lePIO(({ onButtonCltck }) / / Зal'lpaf'IЬI на рендеринг эf'lого компоненf'lа велики, и ,..,, // nblf'/aeмcя избежаf'lь лиwних ol'lpucoвoк. // Здесь мь, npocf'lo симулируем заl'lраl'lный компоненf'I const now = peгforl'lance.now(); wh'\.te (perforl'lance.now() - now < 1000) { // Искуссf'lвенная задержка на 1000 мс => { } гeturn <button onCltck={onButtonCltck}>Cltck Мe</button>; }); const MyCol'lponent = () => { const [count, setCount] = useState(0); const [otherState, setOtherState] = useState(0); // Э,,,01'1 обраl'lный вызов запоминаеf'IСЯ и изменяеf'lся, если f'lолько меняеf'IСЯ const tncrel'lentCount = useCallback(() => { setCount((prevCount) => prevCount + 1); }, []);//Массив зависимосf'lей // Э,,,о обновление сосf'lояния вызовеf'I const doSol'lethtngElse = () => { новый рендеринг MyCol'lponent setOtherState((s) => s + 1); }; retuгn ( <d'\.v> <p>Count: {count}</p> <Expens'\.veCOP1ponent onButtonCltck={tncrel'lentCount} /> <button onCltck={doSol'lethtngElse}>Do Sol'lethtng Else</button> </d'\.v> ); }; count
144 Глава 5 В этом примере: ♦ Expensi.veCOl'lponent является React.l'1el'10, что означает, что дочерним компонентом, который встроен в он будет отображаться лишь в том случае, если из­ менятся его пропсы. Это тот случай, когда вы хотите избежать передачи нового экземпляра функции при каждом отображении; ♦ MyCOl'lponent имеет две части состояния: count и otherState; tncreмentCount это обратный вызов, который обновляет счетчик count. Он за­ useCallback, который означает, что поминается при использовании Expensi.veCOl'lponent не будет повторно отрисовываться, когда MyCOl'lponent начнет выполнять повторный рендеринг из-за изменения otherState; функция doSOl'lethi.ngElse изменяет otherState, но это не обязательно запоминается с помощью useCallback, потому что он не передается в Expensi.veCOl'lponent или лю­ ♦ ♦ бому другому дочернему элементу. Используя useCallback, мы гарантируем, что отрисовываться без необходимости, когда причинам, не связанным с count. Expensi.veCOl'lponent MyCOl'lponent повторно не будет повторно отрисовывается по Это полезно в тех случаях, когда отрисовка дочер­ него компонента является тяжелой и сложной операцией, и вы хотите оптимизиро­ вать производительность за счет сокращения количества отрисовок. Это хороший пример того, как использовать useCa l lback, чтобы избежать повторных рендерингов, гарантируя, что функция, передаваемая дорогостоящему компоненту, создается только один раз, и что он сохраняет одну и ту же ссылку при повторных рендерингах. Так мы предотвращаем ненужный повторный рендеринг дорогостоя­ щего компонента. useCa l lback - это, по сути, хук useМePIO, хранящий информацию об использовании функций. Давайте рассмотрим другой пример: const MyCOl'lponent = () => { const dateOfBi.rth = "1993-02-19"; const i.sAdult = new Date().getFullYear() - new Date(dateOfBi.rth).getFullYear() >= 18; i.f (tsAdult) { retuгn <h1>You аге an adult!</h1>; <h1>You аге а } etse { гetuгn ~i.nor!</h1>; } }; Мы нигде не используем useМel'IO в основном потому, что компонент не имеет со­ стояния. Это хорошо! Но что, если у нас есть какие-то входные данные, которые запускают рендеринг следующим образом: const MyCOl'lponent = () => { const [bi.rthYear, setBi.rthYear] = useState(1993); const i.sAdult = new Date().getFullYear() - bi.rthYear >= 18;
Общие вопросы и мощные шаблоны 145 return ( <dl.V> <laЬel> Bi.rth уеаг: <"input type="nuP1ber" value={bi.rthYear} onChange={(e) => setBi.rthYear(e.target.value)} /> </laЬel> {i.sAdult? <h1>You </d"iv> аге an adult!</h1> <h1>You аге а P1i.nor!</h1>} ); }; Теперь мы пересчитываем new Date() при каждом нажатии клавиши. Давайте испра­ вим это с помощью useMerю: const MyCOP1ponent = () => { const [bi.rthYear, setBi.rthYeaг] = useState(1993); const today = useМeP10(() => new Date(), []); const i.sAdult = today.getFullYear() - bi.rthYear >= 18; return ( <dl.V> <laЬel> Bi.rth уеаг: <"input type="nuP1Ьer" value={bi.rthYear} onChange={(e) => setBi.rthYear(e.target.value)} /> </laЬel> {i.sAdult? <h1>You </dtv> аге an adult!</h1> <h1>You аге а P1i.nor!</h1>} ); }; Это хорошо, потому что today будет ссылаться на один и тот же объект каждый раз, когда компонент отрисовывается с теми же пропсами, и мы предполагаем, что ком­ понент всегда будет отрисовываться повторно в один и тот же день.
Глава 146 ~ 5 Здесь возникает небольшая краевая проблема в случае, если часы пользо­ вателя, использующего этот компонент, перестают работать в полночь, но это редкий случай, который мы можем пока проигнорировать. Естествен­ но, мы должны это поправить, когда речь зайдет о реальном производст­ венном коде. Этот пример поднимает более важный вопрос: должны ли мы переносить значение i.sAdult в useMel'10? Что произойдет, если мы это сделаем? Ответ заключается в том, что мы не должны этого делать, потому что i.sAdult - это скалярное значение, ко­ торое не требует никаких вычислений, а нуждается только в выделении памяти. Мы вызываем де выполнения .getFullYear множество раз, но мы доверяем движку JavaScript и сре­ React, которые справятся с производительностью для нас. Это про­ стое задание, не требующее дополнительных вычислений, таких как сортировка, фильтрация или сопоставление. В этом случае нам не следует использовать хук useMeмo, поскольку это, скорее всего, замедлит работу нашего приложения из-за накладных расходов на использование самого useMeмo, включая его импорт, вызов, передачу зависимостей и последующее сравнение зависимостей с целью увидеть, следует ли пересчитывать значение. Все это сопряжено со сложностями во время выполнения, которые могут скорее навре­ дить нашему приложению, чем помочь ему. Вместо этого мы доверяем React ин­ теллектуальную перерисовку нашего компонента, когда это необходимо, а также и оптимизацию компонента. Наши приложения теперь выигрывают в производительности благодаря более бы­ строму повторному рендерингу даже в условиях больших объемов вычислений. Но можем ли мы добиться большего? В следующем разделе давайте посмотрим, поче­ му все, что мы рассматривали до сих пор, вероятно, даже не будет иметь значения через несколько лет, учитывая некоторые интересные моменты, над которыми ра­ ботает команда React. Разработчики хотят добиться того, чтобы автоматически учитывалась возможность запоминания, позволяя нам забыть о деталях и вместо этого сосредоточиться на наших приложениях. Забудьте обо всем этом React Forget - это новый набор инструментов, предна·Jначснный для автоматюа­ ции запоминания в приложениях React, хуки, как Автоматически обрабатывая 3а11оминание, Forget useMel'10 и useCallback. что потенциально делает ненужными такие React помогает оптимизировать повторный рендерию' компонентов, совершенст­ вуя опыт как пользователя experience, DX). поведения React (user experience, UX), так и разработчика (developer Эта автоматизация при повторном рендеринге перемещает фокус с изменения идентификации объекта на юменение семантического 3начения без глубокого сравнения, тем самым повышая проювол.ителыюсть. Пред­ ставленный на React Conf 2021, React Forget на момент написания книги еще нс был широко доступен, но он используется в проюводственных задачах Meta в Facebook, Instagratn и других социальных сетях и на данный момент "превзошел ожидания" внутри компании.
Общие вопросы и мощные шаблоны Если проявится достаточный интерес, мы расскажем о React Forget 1 147 в следующем издании этой книги. Пожалуйста, дайте нам знать, разместив заявку в социальных сетях (особенно в Х, ранее Twitter) и отметив автора @tejaskumar_. Ленивая загрузка По мере роста наших приложений мы накапливаем большое количество JavaScript- кoдa. Затем наши пользователи загружают эти массивные JavaScript-пaкeты, объем которых иногда исчисляется двузначными числами в мегабайтах, только для того, чтобы использовать небольшую часть этого кода. В этом заключается проблема, потому что происходит замедление начальной загрузки наших приложений, а так­ же оказывает влияние и на работу последующих страниц, т. к. пользователям при­ ходится загружать весь пакет заново. Это особенно существенно, когда у нас нет доступа к серверам, которые обслуживают эти пакеты, и мы не можем добавить необходимые заголовки для кеширования и т. д. Одна из основных проблем, связанных с использованием слишком большого коли­ JavaScript, заключается в том, что это может замедлить загрузку страницы. Файлы JavaScript обычно больше по размеру, чем другие типы веб-ресурсов, такие как HTML и CSS, и требуют больше времени для обработки. Это может привести к чества увеличению времени загрузки страницы, особенно при медленном подключении к Интернету или на старых устройствах. Например, рассмотрим следующий фрагмент кода, который при загрузке страницы скачивает большой файл <!ООСТУРЕ JavaScript: htl'll> <htJII\> <held> <t\.t\1>Мy Webstte</tt.t\1> <scn.pt src="https://exal'lple.cOl'l/large.js"></scn.pt> </htld> <ЬоdУ> < / • • Содержание сl'lраницы • •> </Ьоdу> </htJII\> В этом примере файл largeJs загружается в раздел <head> страницы, и это означает, что он будет запущен раньше любого другого содержимого на странице. Такое по­ ведение может привести к замедлению загрузки всей страницы, особенно при мед­ ленном подключении к Интернету или на старых устройствах. Распространенным решением этой проблемы является асинхронная загрузка JavaScript-фaйлoв с ис­ пользованием атрибута <!ООСТУРЕ <htJII\> htl'll> async:
Глава 148 5 <head> <tltle>My Webstte</tltle> <scгtpt async src="https://exaмple.c01'1/large.js"></scгtpt> </head> <Ьоdу> <!-- Содержание Сf'lраницы --> </Ьоdу> </htмl> В этом примере файл async. largejs загружается асинхронно с использованием атрибута Это означает, что он будет загружаться параллельно с другими ресурсами, что может помочь сократить общее время загрузки страницы. Еще одна проблема, связанная с использованием слишком большого количества JavaScript, заключается в том, что это может увеличить объем использования дан­ ных. Пакеты JavaScript обычно имеют больший размер, чем другие типы веб­ ресурсов, что означает, что для их пересылки по сети требуется передать больше данных. Это может стать проблемой для пользователей с ограниченными тариф­ ными планами или медленным подключением к Интернету, поскольку может при­ вести к увеличению затрат и замедлению загрузки страниц. Для того чтобы устранить эти проблемы, мы можем предпринять несколько шагов для уменьшения объема JavaScript, который предоставляется пользователям. Один из подходов заключается в применении разделения кода с целью загрузки только того JavaScript-кoдa, который необходим для конкретной страницы или функции. Это может помочь сократить время загрузки страницы и использование данных за счет загрузки лишь необходимого кода. Например, рассмотрим следующий фрагмент кода, в котором используется разде­ ление кода для загрузки только того JavaScript, который необходим для конкретной страницы: i.rlpoгt("./large.js").then((мodule) => { // Используй модуль здесь }); В этом примере функция импорта применяется для асинхронной загрузки файла large.js только тогда, когда это необходимо. Это может помочь сократить время загрузки страницы и использование данных, загружая только необходимый код. Другой подход заключается в использовании ленивой загрузки (lazy loading) с це­ лью отложить скачивание некритичного JavaScript-кoдa до тех пор, пока страница не загрузится. Это может помочь сократить время загрузки страницы и использова­ ние данных, загружая некритичный код только тогда, когда это необходимо. Например, рассмотрим следующий фрагмент кода, который использует ленивую загрузку для отсрочки загрузки некритичного <!DОСТУРЕ htмl> <htмl> JavaScript:
Общие вопросы и мощные шаблоны 149 <head> <ti.tle>My Websi.te</ti.tle> </head> <Ьоdу> - -> <button i.d="load-l'lOre">Load l'lOГe content</button> < ! - - Содержимое сf'lраницы <scгi.pt> docul'1ent.getEle1'1entByld("load-1'1ore").addEventLi.stener("cli.ck", () => { i.rlpoгt("./non-cri.ti.cal.js").then((l'lOdule) => { // Используй модуль здесь }); }); </scгi.pt> </Ьоdу> </htl'll> В этом примере функция импорта используется для асинхронной загрузки файла только при нажатии кнопки non-criticaljs Load more content (Загрузить больше содержимого). Это может помочь сократить время загрузки страницы и использо­ вание данных, загружая некритичный код лишь в случае необходимости. К счастью, в React есть решение, которое делает достижение наших целей еще бо­ лее простым: ленивая загрузка с помощью рим, как мы можем использовать их для React. lazy повышения и Suspense. Давайте посмот­ производительности нашего приложения. Ленивая загрузка - это метод, который позволяет нам загружать компонент только тогда, когда это необходимо, как и в случае с динамическим импортом в предыду­ щем примере. Это полезно для больших приложений, содержащих много компо­ нентов, которые не нужны при первоначальном рендеринге. Например, если у нас есть большое приложение со сворачиваемой боковой панелью, содержащей список ссылок на другие страницы, мы можем отказаться от загрузки всей боковой панели, если она сворачивается при первой загрузке. Вместо этого мы можем загружать ее только тогда, когда пользователь обращается к боковой панели. Давайте рассмотрим следующий пример кода: V!poгt Si.debar fгOl'I "./Si.debar"; // Пpeдcf'/OUf'I uмnopf'lupoвaf'lь 22 Мбайf'I const MyCOl'lponent = ({ i.ni.ti.alSi.debarState }) => { const [showSi.debar, setShowSi.debar] = useState(i.ni.ti.alSi.debarState); ( <di.v> <button onCli.ck={() => setShowSi.debar(!showSi.debar)}> Toggle si.debar гetuгn
Глава 150 5 </button> {showSidebaг && <Stdebar />} </dtv> ); }; В этом примере размер шой объем JavaScript <Sidebar /> составляет 22 Мбайт JavaScript-кoдa. Это боль­ для загрузки, разбора и выполнения, и в нем нет необходимо­ сти при первоначальном рендеринге, если боковая панель свернута. Вместо этого React. lazy для ленивой загрузки компонента, только если значение showSidebar будет равно true, т. е. если нам это нужно: 'iPlport { lazy, Suspense} frOl'I "геасt"; 'iPlport FakeSidebaгShell frOl'I "./FakeSidebaгShell"; // Предсrrюиf'I uмnopl'lupoвaf'lь 1 Кбайf'I мы можем использовать const Sidebar = lazy(() => 'iPlport("./Sidebar")); const MyCoмponent = ({ initialSidebarState }) => { const [showSidebar, setShowSidebar] = useState(initialSidebarState); return ( <dtv> <button onClick={() => setShowSidebaг(!showSidebaг)}> Toggle sidebaг </button> <Suspense fallback={<FakeSidebaгShell />}> {showSidebaг && <Stdebaг />} </Suspense> </dtv> ); }; Вместо статического импорта передаем в функцию lazy, . /Sidebar мы импортируем его динамически, т. е. возвращающую обещание (promise), которое преобразу­ ется в импортированный модуль. Динамический импорт возвращает promise, по­ скольку модуль может быть недоступен в данный момент. Возможно, сначала его потребуется загрузить с сервера. Функция React. lazy, которая запускает импорт, никогда не вызывается, если не требуется визуализация базового компонента (в данном случае панели Sidebaг). Таким образом, мы избегаем скачивания боковой панели <Stdebaг /> размером 22 Мбайт до тех пор, пока не потребуется визуализа­ ция этой панели. Возможно, вы также заметили еще одно нововведение: Suspense для размещения компонента в дереве. Suspense - Suspense. Мы используем это компонент, который
Общие вопросы и мощные шаблоны позволяет нам отображать резервный promise (fallback) 151 компонент во время выполнения (читай: во время загрузки боковой панели). В этом фрагменте мы показы­ ваем резервный компонент, представляющий собой облегченную версию тяжелой боковой панели, которая демонстрируется во время загрузки. Это отличный способ обеспечить немедленную обратную связь с пользователем во время загрузки боко­ вой панели. Теперь, когда пользователь нажимает кнопку для переключения на боковую па­ нель, он видит "скелет пользовательского интерфейса" ("skeleton UI"), что помогает ориентироваться во время загрузки и визуализации боковой панели. Улучшенный контроль UI с помощью Suspense React Suspense работает как блок try/catch. Вы знаете, как можно сгенерировать (throw) исключение буквально из любого места в вашем коде, а затем перехватить его с помощью блока catch в другом месте или даже в другом модуле? Что ж, Suspense работает похожим (но не совсем таким же) образом. Вы можете разместить примитивы с отложенной (ленивой) загрузкой и асинхронные примитивы в любом месте вашего дерева компонентов, а затем перехватить их с помощью компонента Suspense в любом месте над ним в дереве, даже если ваша граница Suspense нахо­ дится в совершенно другом файле. Зная это, мы можем выбрать место, где мы хотим отображать состояние загрузки для нашей боковой панели объемом 22 Мбайт. Например, мы можем скрыть все приложение во время загрузки боковой панели - что является довольно плохой идеей, потому что мы блокируем всю информацию о нашем приложении от поль­ зователя только ради боковой панели - или мы можем показать состояние загруз­ ки только для боковой панели. Давайте посмотрим, как мы можем сделать первое (хотя и не должны), просто чтобы понять возможности iJ,ipoгt { lazy, Suspense} frOPI const Si.debar = lazy(() => Suspense: "геасt"; 'U'lf)Oгt("./Si.debar")); const MyCOl'lponent = () => { const [showSi.debar, setShowSi.debar] = useState(fatse); return ( <Suspense fallback={<p>Loadi.ng ... </p>}> <di.V> onCli.ck={() => setShowSi.debar(!showSi.debar)}> Toggle si.debar <Ьutton </Ьutton> {showSi.debar && <Si.debar />} <riai.n>
152 Глава 5 <p>Hello hello welcOl'le, thi.s i.s the app's ~ai.n </Piai.n> </di.v> </Suspense> агеа</р> ); }; Переводя весь компонент в режим ожидания, мы выполняем рируем текст fallback, т. е. демонст­ "Loading ... " до тех пор, пока не будут загружены все асинхронные до­ (promises). Это означает, что все приложение будет скрыто до тех черние элементы пор, пока не будет загружена боковая панель. Это может быть полезно, если мы хотим подождать, пока все будет готово, чтобы открыть пользователю пользова­ тельский интерфейс "во всей красе", но в данном случае это может быть не лучшей идеей, потому что пользователь остается в недоумении, что происходит, и вообще не может взаимодействовать с приложением. Вот почему мы должны использовать Suspense только для того, чтобы обернуть компоненты, которые необходимо загружать ленивой загрузкой, вот так: 'irlpoгt { lazy, Suspense} frOl'I const Si.debar = lazy(() => "геасt"; 'irlpoгt("./Si.debaг")); const MyC01'1ponent = () => { const [showSi.debar, setShowSi.debar] = useState(fatse); гetuгn ( <di.V> <button onCli.ck={() => setShowSi.debar(!showSi.debar)}> Toggle si.debar </button> <Suspense fallback={<p>Loadi.ng ... </p>}> {showSi.debaг && <Si.deЬaг />} </Suspense> <1'1ai.n> <p>Hello hello welcOl'le, thi.s i.s the app's ~ai.n агеа</р> </Piai.n> </di.v> ); }; Граница Suspense (Suspense boundary)- это очень мощный примитив, который может сделать пользовательские интерфейсы более отзывчивыми и интуитивно понятными. Это отличный инструмент, который стоит иметь в своем арсенале. Бо-
Общие вопросы и мощные шаблоны лее того, если в качестве fallback 153 используется высококачественный скелетный пользовательский интерфейс, мы можем дополнительно помочь нашим пользова­ телям понять, что происходит и чего ожидать во время ленивой загрузки наших компонентов, тем самым ориентируя их на интерфейс, с которым они собираются взаимодействовать, до того, как он будет готов. Использование всего этого - от­ личный способ повысить производительность наших приложений и получить мак­ симальную отдачу от React. Далее мы рассмотрим еще один интересный вопрос, который задают многие разра­ ботчики React: Сравнение В React когда нам следует использовать useState, а когда - useReducer? useState и useReducer доступны два способа управления состоянием: useState и useReducer. Оба эти хука используются для управления состоянием компонента. Разница между ними заключается в том, что useState - это хук, который лучше подходит для управления отдельным фрагментом состояния, тогда как useReducer - это хук, ко­ торый управляет более сложным состоянием. Давайте посмотрим, как мы можем useState для управления { useState} frOl'I "react"; использовать V1poгt состоянием компонента: const MyC01"1ponent = () => { const [count, setCount] = useState(0); return ( <di.v> <p>Count: {count}</p> <Ьutton onCltck={() => setCount(count + l)}>lncre111ent</button> </di.v> ); }; В этом примере мы используем useState для управления одним элементом count. Но что будет, если наше состояние станет немного сложнее? Vlpoгt { useState} frOl'I "геасt"; ния: const MyC01"1ponent = () => { const [state, setState] = useState({ count: 0, па111е: "Tejuma", age: 30, }); состоя­
Глава 154 5 return ( <d'\.V> <p>Count: {state.count}</p> <р>Nаме: {state.na~}</p> <p>Age: {state.age}</p> <button onClick={() => setState({ ... state, count: state.count + 1 })}> Increмent </button> </div> ); }; Теперь мы видим, что наше состояние немного сложнее. У нас есть Мы можем увеличить count, count, na~ и age. нажав на кнопку, которая устанавливает состояние но­ вого объекта, обладающего теми же свойствами, что и предыдущее состояние, но с увеличением count на 1. Это очень распространенный шаблон в React. Проблема в том, что это может привести к появлению ошибок. Например, если мы не будем тщательно и осторожно распространять старое состояние, мы можем случайно пе­ резаписать некоторые свойства состояния. Интересный факт: useState useState использует useReducer внутри. Вы можете думать о как об абстракции более высокого уровня можете переопределить useState с помощью useReducer. На самом useReducer, если захотите! деле, вы Если серьезно, вы просто сделаете это с помощью такого кода: '\.Plport { useReducer} fr0/11 "react"; function useState(initialState) { const [state, dispatch] = useReducer( (state, newValue) => newValue, initialState ); return [state, dispatch]; } Давайте рассмотрим тот же пример, но реализованный с помощью '\.Plport { useReducer} frOl'I "react"; ·const initialState = { count: 0, nаме: "Теjuмма", age: 30, }; useReducer:
Общие вопросы и мощные шаблоны 155 const геduсег = (state, action) => { switch (action.type) { case "increl"1ent": return { ... state, count: state.count + 1 }; default: return state; } }; const MyCol"1ponent = () => { const [state, dispatch] = useReducer(гeducer, initialState); return ( <d'iv> <p>Count: {state.count}</p> <p>Na1"1e: {state.na1"1e}</p> <p>Age: {state.age}</p> <button onCHck={O => dispatch({ type: "increl"1ent" })}>Increl"1ent</button> </d'iv> ); }; Кто-то скажет, что это чуть более подробное описание, чем useState, и многие со­ гласятся,. но этого следует ожидать всякий раз, когда кто-то опускается на более низкий уровень в стеке абстракций: чем ниже абстракция, тем более подробный код. В конце концов, абстракции в большинстве случаев предназначены для замены сложной логики синтаксическим сахаром. Итак, поскольку мы можем делать с useState useState то же самое, что и с useReducer, почему бы нам просто не использовать всегда, т. к. это проще? Для ответа на этот вопрос сначала посмотрим на три больших преимущества, кото­ рые предлагает ♦ useReducer. Он отделяет логику обновления состояния от компонента. Сопутствующую функцию геduсег можно протестировать изолированно и повторно использовать в других компонентах. Это отличный способ хранить чистоту и простоту наших . 2 д компонентов и следовать принципу е инственнои ответственности . Мы можем протестировать геduсег следующим образом: describe("reducer", () => { test("should increl"1ent count when given an increl"1ent action", () => { 2 В объектно-ориентированном программировании принцип единственной ответственности (single responsibility principle) обозначает, что каждый объект должен иметь одну ответственность, и она должна быть полностью инкапсулирована в класс. Все его сервисы должны быть направлены исклю­ чительно на обеспечение этой ответственности. - Прим. пер.
Глава 156 5 const initialState = { count: 0, nal'le: "Tejul'11'1a", age: 30, }; const action = { type: "increl'!ent" }; const expectedState = { count: 1, nal'!e: "Tejul'11'1a", age: 30, }; const actualState = reducer(initialState, action); expect(actualState).toEqual(expectedState); }); test("should return the sal'Je object when given an unknown action", о => { const initialState = { count: 0, na111e: "Tejuma", age: 30, }; const action = { type: "unknown" }; const expectedState = initialState; const actualState = reducer(initialState, action); expect(actualState).toBe(expectedState); }); }); В этом примере мы тестируем два сценария: один, в котором действие инкре­ мента передается в reducer, и другой, в котором выполняется неизвестное действие. В первом тесте мы создаем объект начального состояния со значением счетчика, равным 0, и объект действия приращения. Затем мы ожидаем, что значение в результирующем объекте будет увеличено до поставления toEqua l count 1. Мы используем средство со­ для сравнения ожидаемого и фактического состояний объ­ ектов. Во втором тесте мы создаем исходный объект со значением count, равным О, и неизвестный объект действия. Затем мы ожидаем, что результирующее состоя­ ние объекта будет таким же, как и исходного объекта. Мы используем средство сопоставления toBe для сравнения ожидаемого и фактического состояний объек­ тов, поскольку мы проверяем соответствие ссылок.
Общие вопросы и мощные шаблоны 157 Протестировав наш геduсег таким образом, мы можем убедиться, что он работа­ ет корректно и выдает ожидаемый результат при различных сценариях ввода. ♦ Наше состояние и способ его изменения всегда четко отображаются с помощью useReducer, и некоторые могут возразить, что useState способен запутать общий поток обновления состояния компонента через слои деревьев ♦ JSX. это модель, основанная на событиях, что означает, что ее можно useReducer - использовать для моделирования событий, происходящих в нашем приложении, которые затем мы можем отслеживать в каком-либо журнале аудита. Этот жур­ нал аудита можно применять для воспроизведения событий нашего приложе­ нии, чтобы вывести ошибки или реализовать отладку во времени debugging). (time-trave/ Это также позволяет использовать некоторые мощные шаблоны, как отмена/повтор (undo/redo), оптимистичные обновления (optimistic updates) и (analy- аналитическое отслеживание распространенных действий пользователей tics tracking of common user actions) Хотя useReducer - в нашем интерфейсе. отличный инструмент, который стоит иметь в своем арсенале, он не всегда необходим. На самом деле, оказывается, что он в большинстве случаев может оказаться излишним. Итак, когда нам следует использовать useState, а ко­ гда - useReducer? Ответ заключается в том, что это зависит от сложности вашего состояния. Но, надеюсь, что, обладая теперь всей этой информацией, вы сможете принять более обоснованное решение о том, какой из них использовать в вашем приложении. lmmer и эргономика Immer, популярная библиотека React, особенно полезна при решении сложных за­ дач управления состоянием в ваших приложениях. Когда форма состояния является вложенной или сложной, традиционные методы обновления состояния могут стать многословными и привести к ошибкам. Immer помогает справиться с такими слож­ ностями, позволяя вам работать с черновым изменением состояния, обеспечивая при этом неизменность созданного состояния. В приложении React управление состоянием обычно осуществляется с помощью хуков useState или useReducer. В то время как useState подходит для простого со­ стояния, useReducer больше подходит для комплексного управления состоянием, и именно в этом библиотека Immer преуспевает больше всего. При работе с useReducer функция геduсег 3 (редуктор), которую вы предоставляете, должна быть чистой и всегда возвращать новый объект состояния. При работе с вложенными объектами состояния это может привести к многословию кода. Одна­ ко, интегрируя Immer с useReducer через uself"l'lerReducer из библиотеки use-i.mer, вы можете создавать редукторы, которые изменяют состояние напрямую, в то время как на самом деле работают с черновым состоянием, предоставленным 3 Редуктор (reducer) - Immer. Та- чистая функция, которая вычисляет следующее состояние дерева на основании его предыдущего состояния и применяемого действия. - Прим. пер.
158 Глава 5 ким образом, вы сможете создавать более простые и интуитивно понятные функ­ ции редуктора: 'U'lport { useIIТlE!rReducer} frOl'I const i.ni.ti.alState = { user: { naf"le: John Doe age: 28, address: { ci.ty: New Vork country: USA 11 11 11 11 ; , 11 11 11 use-i.1Т1er 11 , , }, }, }; const reducer = (draft, acti.on) => { swt.tch (acti.on.type) { case updateNaf"le draft.user.naf"le = acti.on.payload; 11 11 : Ьгеаk; case updateCi.ty draft.user.address.ci.ty = acti.on.payload; 11 11 : Ьгеаk; // другие вapuaнl'lbl ... default: Ьгеаk; } }; const My(Ol'lponent = () => { const [state, di.spatch] = useil'l'lerReducer(reducer, i.ni.ti.alState); // }; В этом примере useirт,erReducer значительно упрощает функцию редуктора, позво­ ляя напрямую обновлять свойства вложенных состояний, для чего в традиционном редукторе потребовались бы операции spгead или Более того, Immer Object.assi.gn. не ограничивается только использованием можете использовать ее в useState, useReducer. Вы также когда у вас есть сложный объект и вы хотите
Общие вопросы и мощные шаблоны обеспечить неизменность состояния при обновлении. цию produce, Immer 159 предоставляет функ­ которую вы можете применять для создания следующего состояния на основе текущего состояния и набора инструкций: "U'lport produce frOl'1 "i.mer"; "U'lport { useState} frOl'1 "геасt"; const My(Ol'lponent = () => const [state, setState] { = useState(i.ni.ti.alState); const updateNaмe = (newNaмe) setState( produce((draft) => { draft.user.naмe = => { newNaмe; }) ); }; // }; В функции updateNaмe функция которая получает draft produce принимает текущее состояние и функцию, (черновик) состояния. Внутри этой функции вы можете ра­ ботать с черновиком, как если бы он был изменяемым, в то время как Immer гаран­ тирует, что созданное состояние является новым неизменяемым объектом. Способность Immer упрощать обновление состояний, особенно в сложных или вложенных структурах состояний, делает эту библиотеку отличным дополнением к инструментам управления состоянием React, которые обеспечивают более чистый, удобный в обслуживании и менее подверженный ошибкам код. Мощные паперны Паттерны проектирования программного обеспечения (software design pattems) - это широко используемые решения повторяющихся проблем при разработке про­ граммного обеспечения (ПО). Они позволяют решать задачи, с которыми уже стал­ кивались и которые решали другие разработчики, экономя время и усилия в про­ цессе разработки ПО. Они часто представлены в виде шаблонов или руководств по созданию ПО, которые могут использоваться в различных ситуациях. Паттерны проектирования программного обеспечения обычно описываются с использовани­ ем общепринятых терминов и обозначений, что облегчает их понимание и общение между разработчиками. Их можно применять для повышения качества, обучаемо­ сти и эффективности программных систем.
Глава 160 5 Паттерны проектирования программного обеспечения важны по нескольким при­ чинам. Возможность повторного использования. Паттерны проектирования предоставляют многократно используемые решения распространенных проблем, и эти решения могут сэкономить время и усилия при разработке программного обеспечения. Стандартизация. Паттерны проектирования предоставляют стандартный способ решения про­ блем, который облегчает взаимопонимание и взаимодействие разработчиков друг с другом. Удобство сопровождения. Паттерны проектирования позволяют структурировать код, который легко под­ держивать и модифицировать, что может повысить долговечность программных систем. Эффективность. Паттерны проектирования обеспечивают эффективные решения распространен­ ных задач, которые могут повысить производительность программных систем. Обычно паттерны проектирования программного обеспечения появляются естест­ венным образом с течением времени в ответ на потребности реального мира. Они решают конкретные задачи, с которыми сталкиваются инженеры, и входят в "арсе­ нал инструментов инженера" для использования в различных ситуациях. Один паттерн, по своей сути, не хуже другого; у каждого есть свое место. Большинство паттернов помогают нам определить идеальные уровни абстракции. Мы хотим написать код, который стареет "в стиле как хорошее вино", вместо того чтобы накапливать дополнительные состояния и конфигурации до такой степени, что они становятся нечитаемыми и/или недоступными для обслуживания. Вот по­ чему при выборе паттерна проектирования основное внимание уделяется контро­ лю: какую его долю мы предоставляем пользователям, а сколько обрабатывает на­ ша программа? Теперь давайте рассмотрим некоторые популярные паттерны React, следуя прибли­ зительному хронологическому порядку их появления. Компоненты презентации и контейнера Обычно паттерн проектирования React представляет собой комбинацию двух ком­ понентов: компонента презентации и компонента контейнера. Компонент презен­ тации отображает пользовательский интерфейс, а компонент контейнера обрабаты­ вает состояние пользовательского интерфейса. Давайте снова рассмотрим счетчик.
Общие вопросы и мощные шаблоны 161 Вот как должен выглядеть счетчик при реализации этого паттерна: const PresentattonalCounter = (props) => { return ( <secti.on> <button onCltck={props.tncrerient}>+</Ьutton> <button onCltck={props.decrerient}>-</button> <button onCltck={props.reset}>Reset</Ьutton> <h1>Current Count: {props.count}</h1> </secti.on> ); }; const ContatnerCounter = () => { const [count, setCount] = useState(0); const tncrerient = () => setCount(count + 1); const decrerient = () => setCount(count - 1); const reset = () => setCount(0); return ( <Presentati.onalCounter count={count} increrient={increrient} decreмent={decreмent} reset={reset} /> ); }; В этом примере у нас есть два компонента: компонент) и ContatnerCounter PresentattonalCounter (презентационный (контейнерный компонент). Презентационный ком­ понент отображает пользовательский интерфейс, а контейнерный компонент обра­ батывает состояние. Почему это так? Такой шаблон весьма полезен из-за принципа единственной от­ ветственности (рассмотренной выше), который настоятельно рекомендует нам разделять задачи в наших приложениях, что позволяет их лучше масштабировать за счет большей модульности, многоразовости и лучшей тестируемости. Вместо того чтобы поручать компоненту отвечать за то, как он должен выглядеть и как он дол­ жен работать, мы разделяем эти заботы. Что в результате? PresentattonalCounter мо­ жет быть передан между другими контейнерами с отслеживанием состояния и со­ хранить желаемый нами внешний вид интерфейса, в то время как ContatnerCounter может быть заменен другим контейнером с отслеживанием состояния и сохранить функциональность, которая нам нужна.
162 Глава 5 Мы можем изолированно протестировать Contai.nerCounter, но вместо этого можем визуально протестировать Presentati.onalCounter (используя Storybook4 или анало­ гичный инструмент). Мы также можем назначить инженеров или инженерные группы, которым удобнее работать с визуальным оформлением, на разработку Presentati.onalCounter, а инженеров, мы, - на работу с Contai.nerCounter. предпочитающих структуры данных и алгорит­ Благодаря такому независимому подходу у нас появилось гораздо больше возмож­ ностей. По этим причинам шаблон контейнер/презентация приобрел довольно большую популярность и используется до сих пор. Однако внедрение хуков позво­ лило значительно упростить добавление состояния к компонентам без необходимо­ сти использования компонента-контейнера для обеспечения этого состояния. В настоящее время во многих случаях шаблон контейнер/презентация можно заме­ нить хуками. Хотя мы все еще можем применить этот шаблон, даже с помощью React Hooks 5 , при разработке небольших приложений от его использования лучше воздержаться. Компонент более высокого порядка Согласно определению функции высшего порядка в Википедии (https://oreil.lyNwx56): "В математике и информатике функция высшего порядка HOF) - (higher-order function, это функция, которая выполняет по крайней мере одно из следующих дей­ ствий: принимает одну или несколько функций в качестве аргументов (т. е. проце­ дурный параметр, который является параметром функции, которая сама по себе яв­ ляется процедурой), возвращает функцию в качестве результата". В мире JSX компонент высшего порядка (higher-order component, НОС) в основном представляет собой следующее: это компонент, принимающий другой компонент в качестве аргумента и возвращающий новый компонент, который является резуль­ татом объединения этих двух компонентов. Компоненты НОС отлично подходят для совместной работы компонентов, которую мы бы предпочли не повторять. Например, многим веб-приложениям необходимо запрашивать данные из какого­ либо источника данных асинхронно. Ошибки загрузки часто неизбежны, но мы иногда забываем учитывать их в нашем программном обеспечении. Если мы вруч­ ную добавляем пропсы loadi.ng, data и еггог в наши компоненты, вероятность того, что мы пропустим некоторые из них, становится еще выше. 4 Storybook - инструмент JavaScript для организации пользовательских интерфейсов, который делает процессы разработки компонентов, тестирования и создания документации более эффективными и простыми. Он поддерживает множество фреймворков и библиотек для веб-приложсний, в том числе React, Vue и Angular. - Прим. пер. s Хуки (hooks)- механизм в React, который позволяет работать полностью без классов. Он не прино­ сит ничего нового, но облегчает повторное использование кода для решения общих задач. Сейчас это основной способ написания Rеасt-приложений. Но хуки не заменяют собой классы целиком. пер. - Прим.
Общие вопросы и мощные шаблоны 1 163 Давайте рассмотрим базовое приложение для составления списка дел: const Арр = () => { const [data, setData] = useState([]); useEffect(() => { fetch("https://P1ytodol'i.st.coP1/i.teP1s .then((res) => res.json()) .then(setData); }, []); 11 ) return <8astcTodoLtst data={data} />; }; В этом приложении есть проблемы. Мы не учитываем ошибки, возможные при за­ грузке. Давайте исправим это: const Арр = () => { const [tsloadtng, setisLoadtng] = useState(true); const [data, setData] = useState([]); const [еггог, sеtЕггог] = useState([]); useEffect(() => { fetch("https://P1ytodoHst.cOP1/i.te1ТJs 11 ) .then((res) => res.json()) .then((data) => { set!sloadtng(fa\se); setData(data); }) .catch(setError); }, []); return tsloadtng? ( "Loadtng ... " ) : еггог? ( error.P1essage ) : ( <8astcTodoLtst data={data} /> ); }; Упс. Что-то быстро вышло из-под контроля. Более того, решается проблема толь­ ко для одного компонента. Нужно ли нам добавлять эти фрагменты состояния (т. е.
Глава 164 loadi.ng, data 5 и еггог) к каждому компоненту, который взаимодействует с внешним источником данных? Это сквозная проблема, и именно в этом проявляют себя ком­ поненты НОС. Вместо того чтобы повторять эту загрузку и отслеживание ошибок для каждого компонента, который асинхронно взаимодействует с внешним источником данных, мы можем использовать специальную НОС-фабрику для обработки этих состояний. Давайте рассмотрим НОС-фабрику const Todoli.st = wi.thAsync будет wi.thAsync, wi.thAsync(Basi.cTodoli.st); которая устраняет нашу проблему: обрабатывать состояния загрузки и ошибки, а также отображать любой компонент при наличии данных. Давайте рассмотрим его реализацию: const wi.thAsync =(COP1ponent) => tf (props.loadi.ng) { return Loadi.ng ... II 11 => { (ргорs) ; } tf { return error.P1essage; (ргорs.еггог) } return ( <C~t / / Другие пропсы 'COf'lponent' { ... ргорs} ); }; Итак, теперь, когда любой COP1ponent передается в wi.thAsync, мы получаем новый компонент, который отображает соответствующие фрагменты информации на ос­ нове его пропсов. Наш исходный компонент превращается в нечто более работо­ способное: const Todoli.st =wi.thAsync(Basi.cTodoLi.st); const Арр = О => { const [i.sloadi.ng, setlsloadi.ng] =useState(true); const [data, setData] =useState([]); const [еггог, setError] =useState([]); useEffect(() => { fetch( https://1'1ytodoli.st.cOP1/i.tel'1s .then((res) => res.json()) .then((data) => { 11 11 )
Общие вопросы и мощные шаблоны 165 setlsloading(false); setData(data); }) .catch(setError); }, []); гetuгn <TodoLtst loading={isloading} еггог={еггог} data={data} />; }; Больше никаких вложенных файлов. Сам ToDolist может отображать соответст­ вующую информацию о том, загружается ли он, содержит ли ошибку или содержит данные. Поскольку НОС-фабрика w'i.thAsync решает эту сквозную проблему, мы мо­ жем обернуть ею любой компонент, который взаимодействует с внешним источни­ ком данных, и получить новый компонент, который реагирует на пропсы loading еггог. Рассмотрим блог: const Post = wtthAsync(BasicPost); const COl'V'lents = wtthAsync(BasicCOl'V'lents); const Blog = ({ геq }) => { const { loading: isPostloading, req.query.postld еггог: postloadError} = usePost( ); const { loading: aгeCOl'V'lentsloading, postld: req.queгy.postld, }); гetuгn ( <> <Post id={req.query.postld} loading={isPostloading} еггог={роstlоаdЕггог} /> <COl'l'lents postld={req.query.postld} loading={areCOl'V'lentsloading} error={cOl'V'lentloadErroг} /> </> ); }; ехрогt default Blog; еггог: cOl'V'lentloadError} = useCOl'V'lents({ и
Глава 166 5 В этом примере и Post, и Co1'11'1ents используют шаблон НОС wi.thAsync, который воз­ Basi.cPost и Basi.cComents, соответственно, которые те­ перь реагируют на пропсы loadi.ng и еггог. Поведение для этой сквозной задачи цен­ трализованно управляется в реализации wi.thAsync, поэтому мы учитываем вращает более новую версию состояния загрузки и ошибки "бесплатно", просто используя приведенный здесь шаблон НОС. Однако надо отметить, что, как и от презентационных и контейнерных компонен­ тов, от НОС часто отказываются в пользу хуков, поскольку хуки обеспечивают аналогичные преимущества и дополнительные удобства. Создание НОС Совместное создание нескольких НОС - это распространенный шаблон в React, который позволяет разработчикам смешивать функциональные возможности и по­ ведение различных компонентов. Вот пример того, как можно объединить несколь­ ко НОС. Предположим, у вас есть два НОС, wi.thloggi.ng // withLogging.js const wi.thloggi.ng = (WrappedCoмponent) => { гetuгn (ргорs) => { console.log("Rendered wi.th ргорs:", ргорs); гetuгn <WrappedCoмponent { ... ргорs} />; и wi.thUser: }; }; // withUseг.js const wi.thUser = (WгappedCoмponent) => { const user = { паме: "John Doe" }; // Допусf'lим, эf'/о nocf'lynaef'/ из // некоf'lорого исf'lочника данных гetuгn (ргорs) => <WrappedCoмponent { ... ргорs} user={useг} />; } Теперь предположим, что вы хотите объединить эти два НОС. Один из способов сделать это const - вложить один в другой: EnhancedCoмponent = wi.thLoggi.ng(wi.thUser(MyCoмponent)); Однако вложенные вызовы НОС могут быть сложными для чтения и поддержки, особенно по мере увеличения количества НОС. Представьте, что подобная струк­ тура может появиться со временем в вашем приложении: const EnhancedCoмponent = wi.thErrorHandler( wi.thLoadi.ngSpi.nner( wi.thAuthenti.cati.on( wi.thAuthori.zati.on( wi.thPagi.nati.on(
Общие вопросы и мощные шаблоны 1 167 wi.thDataFetchtng( wi.thLoggtng(wi.thUser(wi.thTheмe(wi.thintl(wi.thRouttng(MyCOl'lponent))))) ) ) ) ); Не очень красиво! Лучшим подходом является создание функции, которая объеди­ няет несколько НОС в один НОС. Такая функция может выглядеть следующим об­ разом: // coмpose.js const сомроsе = ( ... hocs) => (Wrapped(Ol'lponent) => hocs.reduceRtght((acc, hoc) => hoc(acc), WrappedCoмponent); // Применение: const EnhancedCoмponent = cOl'lpose(wi.thloggtng, wi.thUser)(MyCoмponent); reduceRtght для применения каждого WrappedCOl'lponent. Таким образом, вы можете пере­ В этой функции сомроsе используется функция НОС справа налево к компоненту числить свои НОС в виде простого списка, который легче читать и поддерживать. Функция cOl'lpose - это распространенная утилита в функциональном программи­ ровании, и библиотеки, такие как Redux, предоставляют для этой цели собственную функцию сомроsе. Вернемся к нашему предыдущему неудачному примеру, вооруженные нашей новой утилитой сомроsе, которая будет выглядеть примерно так: const EnhancedCoмponent wi.thErrorHandler, wi.thloadtngSptnner, wi.thAuthenttcatton, wi.thAuthortzatton, wi.thPagtnatton, wi.thDataFetchtng, wi.thloggtng, wi.thUser, wi.thTheмe, wi.thintl, wi.thRouttng )(MyCoмponent); = сомроsе(
168 Глава 5 Стало лучше, верно? Меньше отступов, больше удобочитаемости и проще в обслу­ живании. Каждый НОС в цепочке дополняет компонент, созданный предыдущим НОС, приобщая к нему собственное поведение. Таким образом, вы можете созда­ вать сложные компоненты из более простых компонентов и НОС, каждый из кото­ рых направлен на решение одной задачи. Это делает ваш код более модульным, понятным и простым в тестировании. НОС в сравнении с хуками С появлением хуков НОС стали менее популярны. Хуки предоставляют более удобный способ добавления функциональности компонентам, а также решают не­ которые проблемы, с которыми сталкиваются НОС. Например, НОС способны вы­ зывать проблемы с переадресацией ссылок, а при неправильном использовании они также могут вызывать ненужную повторную визуализацию. В табл. 5.1 приведено подробное сравнение хуков и НОС. Таблица Характеристика Компоненты высшего 5.1. Сравнение НОС с хуками Хуки порядка (НОС) Повторное использо- Отлично подходят для совмест- вание кода ного использования логики не- извлечения и совместного исполь- сколькими компонентами зования внутри компонента или Идеально подходят для логики между аналогичными компонен- тами Логика рендеринга Могут управлять рендерингом Не влияют непосредственно на обернутого компонента рендеринг, но могут использо- ваться в функциональных компонентах для управления побочны- ми эффектами, связанными с рендерингом Обработка пропсов Могут вводить пропсы и мани- Невозможно напрямую вводить пулировать ими, предоставляя пропсы или манипулировать им дополнительные данные или функции Управление состоя- Могут управлять состоянием Предназначены для управления нием вне обернутого компонента и локальным состоянием функцио- манипулировать им нальных компонентов Методы жизненного Могут инкапсулировать логику useEffect и цикла жизненного цикла, связанную с рабатывать события жизненного обернутым компонентом другие хуки могут об- цикла внутри функциональных компонентов Простота композиции . Могут компоноваться вместе, Легко компонуются и могут ис- но при неправильном управле- пользоваться вместе с другими нии могут привести к "аду в хуками без добавления слоев оболочке" компонентов ("wrapper hell")
Общие вопросы и мощные шаблоны Таблица Характеристика Компоненты высшего 5.1 169 (окончание) Хуки порядка (НОС) Простота тестиро- Тестирование может стать Как правило, легче тестировать, вания более сложным из-за дополни- т. к. легче изолировать по сравне- тельных компонентов оболочки ниюсНОС В Определение типов лучше и про- Безопасность типов TypeScript может быть слож- но правильно определять типы, ще работает с TypeScript особенно в глубоко вложенных нос В табл. 5.1 представлено сравнительное описание хуков, демонстрирующее их пре­ имущества и варианты использования. Хотя НОС по-прежнему являются полезным шаблоном, хуки, как правило, предпочтительнее для большинства случаев приме­ нения благодаря их простоте и удобному использованию. Из этой таблицы мы можем видеть, что для совместного использования логики компоненты НОС и хуки в React играют ключевую роль, но они предназначены для немного разных вариантов использования. НОС преуспевают в совместном исполь­ зовании логики несколькими компонентами и особенно искусны в управлении ото­ бражением обернутого компонента и манипулировании пропсами, предоставляя компонентам дополнительные данные или функции. Они могут управлять состоя­ нием за пределами обернутого компонента и инкапсулировать логику жизненного цикла, связанную с обернутым компонентом. Однако при неправильном управле­ нии они могут привести к "аду в оболочке", особенно при глубоком вложении НОС друг в друга. Такая вложенность также может усложнить тестирование, а поддерж­ ка безопасности типов с помощью TypeScript может стать сложной задачей, осо­ бенно при использовании глубоко вложенных НОС. С другой стороны, хуки идеально подходят для извлечения и совместного исполь­ зования логики внутри компонента или между аналогичными компонентами без добавления слоев дополнительных компонентов, что позволяет избежать сценария под названием "ад в оболочке". В отличие от НОС, хуки не влияют на рендеринг напрямую и не могут непосредственно вставлять пропс или манипулировать им. Они предназначены, в частности, для управления локальным состоянием функцио­ нальных компонентов и обработки событий жизненного цикла с помощью хука useEffect. Хуки упрощают компоновку и, как правило, легче тестируются, посколь­ ку их проще изолировать по сравнению с НОС. Кроме того, при использовании с TypeScript хуки обеспечивают лучшую типизацию, что потенциально уменьшает количество сбоев, связанных с ошибками типов. Хотя и НОС, и хуки предоставляют механизмы для повторного использования ло­ гики, последние предлагают более прямой и менее сложный подход к управлению состоянием, событиями жизненного цикла и другими возможностями в функцио­ нальных компонентах React. С другой стороны, НОС предоставляют более струк-
Глава 170 5 турированный способ внедрения поведения в компоненты, что может быть полезно в кодах большого объема или в кодах, которые еще не используют хуки. Каждый подход имеет свой набор преимуществ. Выбор между использованием НОС или хуков во многом зависит от конкретных требований вашего проекта и от того, на­ сколько команда знакома с тем или другим шаблоном. Можем ли мы вспомнить примеры НОС, которые мы используем достаточно час­ то? Да, мы можем! React.l'lef'IO- это компонент, который мы только что рассмотре­ ли в данной главе, и он действительно представляет собой НОС! Давайте рассмот­ рим другой вариант: React. forwardRef. Это НОС, который перенаправляет ссылку дочернему компоненту. Давайте рассмотрим пример: const Fancyinput = React.forwardRef((pгops, геf) => ( <i.nput type="text" ref={ref} {... ргорs} /> )); const Арр = () => { const inputRef = useRef(nutt); useEffect(() => { inputRef.current.focus(); }, []); ( <di.v> <Fancyinput ref={inputRef} /> </di.V> гetuгn ); }; В данном примере мы используем ту Fancyinput. React. forwardRef для отправки ссылки компонен­ Это позволяет нам получить доступ к методу родительском компоненте. Это распространенный focus элемента input в паттерн в React и отличный пример того, как НОС можно использовать для решения задач, которые трудно ре­ шить с помощью обычных компонентов. Пропсырендеринга Поскольку мы уже говорили о выражениях JSX, общим правилом является исполь­ зование пропсов, которые представляют собой функции, получающие в качестве аргументов состояние на уровне компонентов, что облегчает повторное использо­ вание кода. Вот простой пример: <Wt.ndowSi.ze render={({ wi.dth, height }) => ( <di.v>
Общие вопросы и мощные шаблоны 171 Your wi.ndow i.s {wi.dth}x{hei.ght}px </d"i..v> )} Обратите внимание, что мы имеем пропс с именем render, который получает функ­ цию в качестве значения. Этот пропс даже выводит некоторую разметку рая фактически визуализируется. Но почему? Выходит, Wi.ndowSi.ze JSX, кото­ выполняет неко­ торую волшебную работу по вычислению размера пользовательского окна, а затем вызывает props. render, чтобы вернуть объявленную нами структуру, используя вложенное состояние для отображения размера окна. Давайте взглянем на Wi.ndowSi.ze, чтобы понять это немного лучше: const Wi.ndowSi.ze = (props) => { const [si.ze, setSi.ze] = useState({ width: -1, hei.ght: -1 }); useEffect(() => { const handleResi.ze = () => { setSi.ze({ width: wi.ndow.i.nnerWi.dth, hei.ght: wi.ndow.i.nnerHei.ght }); }; wi.ndow.addEventli.stener("resi.ze", handleResi.ze); return () => wi.ndow.reмoveEventli.stener("resi.ze", handleResi.ze); }, []); return props.render(si.ze); }; Из этого примера мы можем видеть, что Wi.ndowSi.ze использует слушателя событий, чтобы сохранять некоторые данные в состоянии при каждом изменении размера, но сам компонент не имеет заголовка: у него нет мнения о том, какой пользователь­ ский интерфейс представлять. Вместо этого он передает управление любому роди­ тельскому элементу, выполняющему его рендеринг, и вызывает пропс render, эф­ фективно передавая управление родительскому элементу для выполнения задания рендеринга. Это помогает компоненту, который зависит от размера окна для рендеринга, полу­ чать информацию, не дублируя блоки лее "сухим" шаблон уже useEffect, и помогает сохранять наш код бо­ (DRY, Don't Repeat Yourse\f- не повторяйтесь). Правда, теперь этот не так популярен и эффективно заменяется хуками React. Дочерние элементы как функции Поскольку пса render chi.. ldren - это пропс, некоторые предпочли вообще отказаться от про­ и вместо него использовать только дочерние элементы.
172 Глава 5 Это изменило бы использование Wi.ndowSi.ze и привело бы к следующему коду: <Wi.ndowSi.ze> {({ wt.dth, hei.ght }) => ( <di.V> Your wt.ndow i.s {wt.dth}x{hei.ght}px </di.V> )} </WindowSi.ze> Некоторые авторы ветствует Context, замыслу React предпочитают такой подход, потому что это больше соот­ кода: Wi.ndowSi.ze в этом случае немного напоминает React и все, что мы отображаем, имеет тенденцию ощущаться как дочерние эле­ менты, которые используют этот контекст. Тем не менее хуки React полностью устраняют необходимость в этом паттерне, так что, работая в этом направлении, имейте это в виду. Паперн Паттерн Control Props Control Props в React- это стратегический подход к управлению состоя­ нием, который расширяет концепцию управляемых компонентов. Он предоставляет гибкий механизм для определения того, как управляется состояние внутри компо­ нента. Для того чтобы понять это, давайте сначала разберемся с управляемыми компонентами. Управляемые компоненты - это компоненты, которые не поддерживают собст­ венное внутреннее состояние. Вместо этого они получают свое текущее состояние в качестве пропса от родительского компонента, который является единственным источником достоверной информации о состоянии дочерних элементов. Когда со­ стояние должно измениться, управляемые компоненты уведомляют родительский компонент, используя функции обратного вызова, обычно onChange. Таким образом, родительский компонент отвечает за управление состоянием и обновление значе­ ния управляемого компонента. Например, управляемый элемент <1.nput> выглядит следующим образом: functi.on Forм() { const [i.nputValue, setinputValue] = React.useState(""); functi.on handleChange(event) { setinputValue(event.target.value); } return <i.nput type="text" value={i.nputValue} onChange={handleChange} />; }
Общие вопросы и мощные шаблоны Паперн Control Props 173 развивает принцип управляемых компонентов, позволяя компоненту либо управляться извне с помощью пропсов, либо управлять собствен­ ным состоянием, предоставляя необязательный внешний контроль. Компонент, следующий паперну Control Props, принимает как значение состояния, так и функ­ цию для обновления этого состояния в виде пропса. Эта двойная возможность по­ зволяет родительскому элементу осуществлять контроль состояния дочернего ком­ понента, если он того пожелает, но также дает возможность дочернему компоненту работать независимо, если тот не хочет, чтобы его контролировали. Примером паперна Control Props является кнопка-переключатель, которая может либо контролироваться своим родительским элементом, либо управлять собствен­ ным состоянием: functi.on Toggle({ on, onToggle }) { const [tsOn, setlsOn] = React.useState(false); const handleToggle = () => { const nextState = on === undeftned? !tsOn tf (on === undeft.ned) { setisOn(nextState); on; } tf (onToggle) { onToggle(nextState); } }; гetuгn ( onClt.ck={handleToggle}> {on !== undeftned? on: t.son? "On" <Ьutton "Off"} </Ьutton> ); } В Тоgglе-компоненте tsOn отображает внутреннее состояние, в то время как on - это пропс внешнего управления. Компонент может работать в управляемом режиме, если родителем предоставляется пропс В противном случае компонент возвра­ on. щается к своему внутреннему состоянию - t.sOn. Пропс onToggle - это обратный вызов, который позволяет родительскому компоненту реагировать на изменения состояния, предоставляя родительскому вать собственное состояние с состоянием компоненту возможность синхронизиро­ Toggle. Этот паперн повышает гибкость компонента, предлагая как управляемый, так и неконтролируемый режимы работы. Он разрешает родительскому элементу управ­ лять, когда это необходимо, и в то же время позволяет компоненту сохранять авто­ номность в отношении собственного состояния, когда оно явно не контролируется.
Глава 174 5 Коллекции пропсов Нам часто приходится собирать целую кучу пропсов вместе. Например, при созда­ нии пользовательских интерфейсов с возможностью перетаскивания (drag-and-drop) требуется управлять довольно большим количеством пропсов. onDгagStaгt Для указания браузеру, что делать, когда пользователь начинает перетаскивать элемент. OnDragOver Для определения зоны перетаскивания. onDrop Для выполнения некоторого кода при сбрасывании элемента на этот элемент. onDragEnd Для того чтобы сообщить браузеру, что делать, когда перетаскивание элемента завершено. Более того, данные/элементы по умолчанию нельзя перетащить на другие элемен­ ты. Для того чтобы разрешить расположение одного элемента поверх другого, мы должны запретить обработку элемента по умолчанию. Это делается путем вызова метода event.preventDefautt для события onDragOver с целью определения возможной зоны перетаскивания. Поскольку перечисленные выше пропсы обычно применяются вместе, а для onDragOver, как правило, используется значение по умолчанию event => { event.preventDefautt(); PIOreStuff(); }, мы можем собрать эти пропсы вместе и повторно использовать их в различных компонентах, например, так: export const dгорраЫеРгорs = { onDragOver: (event) => { event.preventDefautt(); }, onDrop: (event) => {}, }; export const draggaЫeProps = { onDragStart: (event) => {}, onDragEnd: (event) => {}, }; Теперь, если у нас есть компонент React, который, как мы ожидаем, будет вести себя подобно целевой зоне перетаскивания, мы можем использовать для него кол­ лекцию пропсов следующим образом: <Dropzone { ... dгорраЫеРгорs} /> Это шаблон коллекции пропсов, который позволяет использовать некоторые про­ псы повторно. Он довольно широко применяется для включения ряда пропсов типа
Общие вопросы и мощные шаблоны 175 агi.а-* в доступные компоненты. Однако одна из проблем, которая все еще присут­ ствует, заключается в том, что если мы напишем пользовательский пропс onDragOver и переопределим коллекцию пропсов, мы потеряем событие event.preventDefault, которое мы получаем из коробки, используя коллекцию пропсов. Это может привести к непредвиденному поведению, лишая нас возможности пере­ тащить компонент на Dropzone: <Dropzone { ... dгорраЫеРгорs} onDragOver={() => { alert("Dragged!"); }} /> К счастью, мы можем исправить это с помощью геттеров 6 пропсов. Геттеры пропсов Геттеры пропсов, по сути, объединяют коллекции пропсов с пользовательскими пропсами. В нашем примере мы хотели бы сохранить вызов event.preventDefault в обработчике onDragOver коллекции dгорраЫеРгорs, а также добавить к нему пользова­ тельский alert("Dragged! "). Мы можем сделать это с помощью геттера пропсов. Во-первых, мы изменим коллекцию dгорраЫеРгорs на геттер пропсов: const гetuгn { ехрогt getDroppaЫeProps = () => { onDragOver: (event) => { event.preventDefault(); }, onDrop: (event) => {}, }; } На данный момент ничего не изменилось, кроме того, что если раньше мы экспор­ тировали коллекцию пропсов, то теперь экспортируем функцию, которая возвра­ щает коллекцию пропсов. Это и есть геттер пропсов. Поскольку это функция, она onDragOver. Мы можем скомпоновать этот пользовательский onDragOver с нашим onDragOver по умолчанию, например, так: может получать аргументы, например const col'1pose = ( ... functi.ons) => 6 Геттер (от англ. getter - получатель) - в программировании специальный метод, позволяющий получить данные, доступ к которым напрямую ограничен. Это один из методов объектно-ориенти­ рованного программирования, который помогает реализовать гибкий механизм инкапсуляции. В паре с сеттером (setter) он может использоваться для организации свойств и методов в языках, где они напрямую не поддерживаются. - Прtш. пер.
Глава 176 5 ( ... args) => functtons.forEach((fn) => fn?.( ... args)); ехрогt const getDroppaЫeProps = ({ onDragOver: replacerientOnDragOver, ... replacerientProps }) => { const defaultOnDragOver = (event) => { event.preventDefault(); }; { onDragOver: c0111pose(replacerientOnDragOver, defaultOnDragOver), onDrop: (event) => {}, ... replacerientProps, гetuгn }; }; Теперь мы можем использовать геттер пропсов следующим образом: <Dгopzone { ... getDroppaЫeProps({ onDragOver: () => { alert("Dragged! "); }, })} /> Этот пользовательский onDragOver будет преобразован в наш нию, и оба действия будут выполнены: onDragOver по умолча­ event.preventDefault() и alert("Dragged! "). Это паттерн "геттер пропсов". Составные компоненты Иногда мы можем встретить компоненты-аккордеоны (accordion component), добные этому: <Accoгdton HE!l'1s={[ { laЬel: "One", content: "lorel'I tpsul'I for P10re, see https://one.col'I" }, { laЬel: "Two", content: "lorE!l'1 tpsul'I for l'IOre, see https://two.cOl'I" }, { laЬel: "Three", content: "lorE!l'1 tpsul'I for l'IOГe, see https://three.coP1 ]} 11 }, по­
Общие вопросы и мощные шаблоны 177 Этот компонент предназначен для отображения списка, за исключением того, что в заданный момент времени может быть открыт только один элемент: ♦ One; ♦ Two; ♦ Three. Внутренняя работа этого компонента будет выглядеть примерно так: const Accordion = ({ ехрогt iteмs }) => { [activeiteмindex, setActiveiteмindex] const = useState(0); ( гetuгn <Ul> index) => ( <lt onClick={() => setActiveiteмindex(index)}key={iteм.id}> {iteмs.мap((iteм, <stгong>{tteм.laЬel}</stгong> {index === </lt> activeiteмindex && i.content} ))} </Ul> ); } А если бы мы захотели применить пользовательский разделитель между элемента­ ми и Two Three? Что, если бы мы захотели, чтобы третья ссылка была красной или что-то в этом роде? Мы, вероятно, прибегли бы к какому-нибудь подобному способу: <Accordton Немs={[ { label: "One", content: "loreм ipsuм for маге, see https://one.coм" }, { laЬel: "Two", content: "lогем ipsuм for моге, see https://two.coм" }, { label: "-- -" }, { label: "Three", content: "lогем ipsuм for маге, see https://three.coм"}, ]} /> Но это выглядело бы не так, как мы хотим. Поэтому мы, вероятно, сделали бы больше хаков 7 : ехрогt const Accordion = ({ const 7 iteмs }) => { [activeiteмlndex, setActivelteмlndex] Хак (от англ. англ. hack; родственно слову hacker) - ровании. - Прим. пер. = useState(0); обходное техническое решение в программи­
Глава 178 5 return ( <Ul> index) => {iteмs.мap((iteм, === <hr /> iteм 11 11 ? ( ) : ( <li onClick={() => setActivelteмindex(index)} key={iteм.id}> <strong>{iteм.label}</strong> {index === </li> && i.content} activelteм!ndex ) )} </ul> ); }; Итак, это ли код, которым мы могли бы гордиться? Я не уверен. Вот почему нам нужны составные компоненты: они позволяют нам группировать взаимосвязан­ ные, отдельные компоненты, которые имеют общее состояние, но могут отобра­ жаться раздельно, что дает нам больше контроля над деревом элементов. Аккордеон, выраженный с помощью шаблона составных компонентов, выглядел бы следующим образом: <Accordion> <Accoгdionlteм iteм={{ <Accoгdionlteм iteм={{ <Accoгdionlteм iteм={{ label: One /> label: Two /> 11 11 label: Тhгее }} /> 11 11 11 11 }} }} </Accoгdion> Если мы хотим узнать, как этот шаблон может быть реализован в React, мы можем рассмотреть два способа: ♦ с помощью React.cloneEleмent для дочерних элементов; ♦ с помощью React Context. React.cloneEleмent считается устаревшим щью React Context. прочитана каждая часть аккордеона: const AccoгdionContext activelteмlndex: = cгeateContext({ 0, setActiveiteмindex: }); API, поэтому давайте продолжим с помо­ Во-первых, мы начнем с контекста, из которого может быть () => 0,
Общие вопросы и мощные шаблоны Теперь наш компонент Accordi.on 1 179 просто предоставит контекст своим дочерним эле­ ментам: export const Accordi.on = ({ i.teмs }) => { const [acti.veiteмindex, setActi.veiteмindex] = useState(0); return ( <AccordtonContext.Provi.der value={{ acti.veit~Index, <Ul>{chi.ldren}</ul> </AccordtonContext.Provi.der> setActi.veiteмindex }}> ); }; Далее давайте создадим отдельные компоненты Accordi.oniteм, которые также будут использовать этот контекст и реагировать на него: export const Accordi.onit~ = ({ i.teм, i.ndex }) => { // Замечание: эдесь f,1Ь/ используем кон~екс~, а не сос~ояние! const { acti.veiteмindex, return ( <lt onCli.ck={() => setActi.veit~Index} = useContext(Accordi.onContext); setActi.veiteмindex(i.ndex)} key={i.teм.i.d}> <strong>{i.teм.laЬel}</strong> {i.ndex === </lt> acti.veiteмindex && i..content} ); }; Теперь, когда у нас есть несколько деталей для нашего аккордеона, что делает его составным компонентом, мы трансформируем его из: <Accordton i.teмs={[ { laЬel: "One", content: "lогем i.psuм fог f'ЮГе, see https://one.coм" }, { laЬel: "Two", content: "lогем i.psuм fог f'Юre, see https://two.coм" }, { label: "Тhгее", content: "lогем i.psuм fог f'ЮГе, see https://three.coм" }, ]} /> в такой вид: <Accordton> {i.t~s.мap((i.t~, <Accordi.onit~ ))} </Accordton> i.ndex) => ( key={i.teм.i.d} i.t~{i.teм} i.ndex={i.ndex} />
Глава 180 5 Преимущество этого в том, что у нас гораздо больше возможностей для контроля, т. к. каждый элемент Accordi.onlteм знает о состоянии Accordi.on в целом. Итак, если бы мы захотели провести горизонтальную линию между элементами Two и Three, мы могли бы отказаться от мар и перейти к ручному управлению: <Accordt.on> <Accordt.oniteri <Accordt.oniteri <hr /> <Accordt.oniteri </Accordt.on> key={i.teмs[l].i.d} i.teм={i.teмs[l]} i.ndex={0} /> i.ndex={l} /> key={i.teмs[2].i.d} i.teм={i.teмs[2]} i.ndex={2} /> key={i.teмs[0].i.d} i.teм={i.teмs[0]} Или мы могли бы сделать что-то более гибридное, например: <Accordt.on> {i.teмs.sli.ce(0, 2).мар((i.tем, <Accord"i.oniteri i.ndex) => ( key={i.teм.i.d} i.teм={i.teм} i.ndex={i.ndex} /> ))} <hr /> {i.teмs.sli.ce(2).мap((i.teм, <Accordt.oniteri i.ndex) => ( key={i.teм.i.d} i.teм={i.teм} i.ndex={i.ndex} /> ))} </Accordt.on> В этом преимущество составных компонентов: они передают управление ренде­ рингом родительскому элементу, сохраняя при этом контекстную информацию о состоянии дочерних элементов. Тот же подход можно применять для пользователь­ ского интерфейса с вкладками, где вкладки знают о своем текущем состоянии и имеют различные уровни вложенности элементов. Еще одним преимуществом является то, что такая схема способствует разделению задач, а это помогает приложениям значительно лучше масштабироваться по мере усложнения. Паперн State Reducer Паттерн State Reducer был изобретен и популяризирован Кентом К. Доддсом (Kent Dodds, @kentcdodds), одним из самых выдающихся и опытных инженеров и преподавателей в области React и настоящим экспертом с мировым именем в этой С. сфере. Этот паттерн предлагает эффективный способ создания гибких и настраи­ ваемых компонентов. Давайте проиллюстрируем эту концепцию на реальном при­ мере: компоненте с переключаемой кнопкой. В примере будет продемонстрирова­ но, как можно усовершенствовать базовый компонент toggle, чтобы пользователи могли настраивать логику его состояния, отключая переключатель в определенные дни недели на некоторое время по деловым соображениям.
Общие вопросы и мощные шаблоны Мы начнем с базового компонента переключателя, использующего хук 181 useReducer. Компонент поддерживает собственное состояние, определяя, находится ли пере­ ключатель в Оп- или Оff-положении. Исходное состояние имеет значение false, что указывает на выключенное состояние Off: Vlport React, { useReducer} frOl'I "react"; function toggleReducer(state, action) { swi.tch (action.type) { case "TOGGLE": гetuгn { оп: !state.on }; default: thгow new Error('Unhandled action type: ${action.type}'); } } function Toggle() { const [state, dispatch] = useReducer(toggleReducer, { оп: false }); ( <button onCHck={ () => dispatch( { type: "TOGGLE" }) }> {state.on? "Оп" : "Off"} гetuгn </Ьutton> ); } Для реализации паттерна State Reducer компонент Toggle модифицируется, чтобы stateReducer. Этот пропс позволяет кастомизировать или расширить логику состояния компонента. Функция internalDispatch компонента принять пропс внутреннюю объединяет логику внутреннего редуктора с внешним редуктором, предоставляе­ stateReducer: function Toggle({ stateReducer }) { const [state, dispatch] = useReducer( (state, action) => { const nextState = toggleReducer(state, action); гetuгn stateReducer(state, { ... action, changes: nextState }); мым пропсом }, { оп: false} ); гetuгn <button onCHck={ () => interna lDispatch( { type: "TOGGLE" }) }>
Глава 182 5 {state.on? "On" </button> "Off"} ); } Toggle.defaultProps = { stateReducer: (state, action) => state, // // По умолчанию редукмор не делаем ничего особенного }; Из этого фрагмента кода мы можем видеть, что пропс stateReducer используется для stateReducer вызы­ вается с текущим состоянием и объектом действия acHon; кроме этого, мы добавля­ ем к действию дополнительное свойство метаданных changes. Свойство changes со­ настройки внутренней логики состояния компонента. Функция держит следующее состояние компонента, которое вычисляется внутренним редуктором. Это позволяет внешнему редуктору получать доступ к следующему состоянию компонента и принимать решения на его основе. Давайте посмотрим, как можно использовать компонент Toggle с пользовательским поведением, основанным на этом шаблоне. В следующем примере компонент Арр использует содержит Toggle логику, и предоставляет пользовательский которая предотвращает stateReducer. отключение Этот редуктор переключателя по средам, поскольку среда в этом приложении является универсальным днем "без отключе­ ний". Вот код, который иллюстрирует, как шаблон редуктора состояния позволяет гибко изменять поведение компонента без изменения самого компонента: func"ti.on Арр() { const custo~Reducer = (state, action) => { // Пользовамельская логика: не допусмимь омключение по средам tf (new Date().getDay() === 3 && !changes.on) { return state; } return action.changes; }; return <Toggle stateReducer={custo~Reducer} />; } На этом примере мы видим силу паттерна State Reducer при создании очень гибких и повторно используемых компонентов. Позволяя внешней логике интегрироваться с внутренним управлением состоянием компонента, мы можем удовлетворить ши­ рокий спектр требований к поведению и вариантам использования, повышая как полезность, так и универсальность компонента. Ух ты! Вот это глава! Давайте подведем итоги и обобщим то, что мы узнали.
Общие вопросы и мощные шаблоны 183 Обзор главы На протяжении всей этой главы мы обсуждали различные аспекты React, включая запоминание, ленивую загрузку, редукторы и управление состоянием. Мы рассмот­ рели преимущества и потенциальные недостатки различных подходов к этим темам и то, как эти инструменты могут повлиять на производительность и удобство об­ служивания приложений React. Мы начали с обсуждения запоминания в React и его преимуществ в оптимизации отображения компонентов. Мы рассмотрели функцию React.мe1110 и то, как ее можно использовать для предотвращения ненужных повторных рендерингов компонентов. Мы также рассмотрели некоторые потенциальные проблемы с запоминанием, такие как устаревшее состояние и необходимость тщательного управления зависимостями. Далее мы поговорили о ленивой загрузке в React и о том, как ее можно использо­ вать для отсрочки загрузки определенных компонентов или ресурсов до тех пор, пока они действительно не понадобятся. Мы рассмотрели компоненты Suspense и React. Мы React. lazy и как их можно использовать для реализации загрузки в приложении также обсудили недостатки ленивой загрузки, такие как повышенная сложность и потенциальные проблемы с производительностью. Затем мы перешли к редукторам и тому, как их можно использовать для управле­ ния состоянием в React. Мы рассмотрели различия между useState и useReducer и обсудили преимущества использования централизованной функции редуктора для управления обновлениями состояния. Для того чтобы проиллюстрировать обсуждаемые концепции, на протяжении всего нашего разговора мы использовали примеры кода из наших собственных реализа­ ций. Мы изучили, как эти примеры работают на практике и как они могут повлиять на производительность и удобство сопровождения приложений React. Используя примеры кода и подробные объяснения, мы получили более глубокое представление об этих темах и о том, как их можно применять в реальных прило­ жениях React. Проверьте ваши знания Давайте зададим себе несколько вопросов, чтобы проверить наше понимание кон­ цепций, которые мы изучили в этой главе: 1. Что такое запоминание в React и как его можно использовать для оптимизации рендеринга компонентов? 2. Каковы преимущества использования React 3. и чем он отличается от useReducer для управления состоянием в useState? Как можно реализовать ленивую загрузку в приложении понентов React. lazy и Suspense? React с помощью ком­
184 4. Глава Какие потенциальные проблемы могут возникнуть при использовании запоми­ нания в 5. 5 React и как их можно устранить? Как можно использовать хук useCallback для запоминания React? функций, передавае­ мых в качестве пропсов компонентам в Что дальше? В следующей главе мы исследуем работу React на стороне сервера - рассмотрим серверный рендеринг, его преимущества и компромиссы, гидратацию, фреймворки и многое другое. До встречи!
ГЛАВА Серверный С момента своего создания React 6 React претерпел значительные изменения. Хотя изна­ чально он был клиентской библиотекой, спрос на рендеринг на стороне сервера (server-side rendering, SSR) со временем вырос по причинам, которые мы разберем в этой главе. Здесь же мы рассмотрим серверные библиотеки понять, чем он отличается от клиентского повышения уровня наших приложений React React. Как мы обсуждали в предыдущих главах, нией Meta React React и постараемся и как его можно использовать для изначально разрабатывался компа­ для удовлетворения потребности в эффективных и масштабируемых пользовательских интерфейсах. В главе годаря виртуальному DOM, 3 мы рассмотрели, как это достигается бла­ который позволяет разработчикам с легкостью созда­ вать компоненты пользовательского интерфейса и управлять ими. Клиентский под­ ход React обеспечивает быстрое и отзывчивое взаимодействие с пользователями в Интернете. Однако по мере развития глобальной сети ограничения рендеринга на стороне клиента становились все более очевидными. Ограничения клиентского рендеринга С момента первого выпуска в качестве программного обеспечения с открытым ис­ ходным кодом в 2013 году, в React начали проявляться некоторые ограничения рендеринга на стороне клиента. Эти ограничения в конечном итоге привели к тому, что мы все больше и больше забот начали переносить на сторону сервера. SEO Одним из существенных ограничений рендеринга на стороне клиента является то, что поисковые роботы могут неправильно индексировать контент, поскольку неко­ торые из них не выполняют коды на JavaScript, а те, которые выполняют JavaScript, могут делать это не так, как мы ожидаем. Учитывая большое разнообразие реализаций поисковых систем, а также тот факт, что многие из них являются частными и неизвестны широкой публике, это делает
Глава 6 186 рендеринг только на стороне клиента несколько сомнительным в масштабе данного веб-сайта или приложения. Тем не менее в статье Search Engine Land за 2015 год описывались некоторые экс­ перименты по проверке того, как различные поисковые системы работают с кли­ ентскими приложениями, и вот что упомянули авторы: "Мы провели серию тестов, которые подтвердили, что поисковая система способна выполнять и индексировать же подтвердили, что DOM, Google JavaScript может отображать всю страницу целиком и считывать тем самым индексируя динамически генерируемый контент". Авторы выясняли, что на момент написания статьи поисковые системы Bing Google во множестве реализаций. Мы так­ Google и были достаточно продвинутыми, чтобы индексировать веб-сайты, предназна­ ченные только для клиентского рендеринга, но в конечном счете это всего лишь один исследовательский проект в огромном и непознаваемом океане частной ин­ формации. Таким образом, хотя клиентские приложения могут хорошо работать с современ­ ными поисковыми системами, отсутствие серверного аналога сопряжено с опреде­ ленным риском. В традиционных веб-приложениях, когда пользователь или поис­ ковый робот запрашивает страницу, сервер отображает НТМL-код страницы и отправляет его автору запроса. НТМL-код охватывает все содержимое, ссылки и данные, позволяющие поисковым системам легко считывать и индексировать со­ держимое для получения результатов поиска, поскольку все содержимое страницы представляет собой не что иное, как текст, т. е. разметку в текстовом формате. Однако в случае приложения с рендерингом на стороне клиента, часто созданном с использованием библиотек или фреймворков, таких как React, сервер отправляет обратно почти пустой НТМL-файл, единственной задачей которого является за­ грузка JavaScript из отдельного JavaScript-фaйлa на том же или альтернативном сервере. Затем JavaScript-фaйл загружается и выполняется в браузере, отображая динамичное содержание страницы. Такой подход обеспечивает удобство работы с пользователем, напоминая работу с нативным приложением. Однако у него есть обратная сторона, когда речь заходит о поисковой оптимизации optimization, SEO) (search engine и производительности: мы не загружаем ничего полезного для читателей по первому запросу и вдобавок к этому должны выполнить еще один за­ прос сразу после загрузки страницы, чтобы загрузить JavaScript, который будет ра­ ботать на всем сайте. Это называется сетевым каскадом, или водопадом (network waterfall). Таким образом, еще одним недостатком рендеринга только на стороне клиента яв­ ляется производительность. Давайте поговорим об этом. Производительность Приложения, отображаемые на стороне клиента, могут испытывать проблемы с производительностью, особенно в медленных сетях или на маломощных устройст­ вах. Необходимость загрузки, синтаксического анализа и выполнения JavaScript-
Серверный React 187 кода перед рендерингом содержимого может привести к значительным задержкам в отображении страниц. Это "время ожидания интерактивности" является важней­ шим показателем, поскольку оно напрямую влияет на вовлеченность пользователей и показатели отказов (т. е. на скорость, с которой пользователи покидшJт страни­ цу). Пользователи могут покинуть страницу, если она загружается слишком долго, и такое поведение может еще более негативно сказаться на SЕО-рейтинге страницы. Кроме того, если устройство маломощное и приложение имеет минимальную дос­ тупность к процессору, рендеринг только на клиенте также создает проблемы для пользователя. Это связано с тем, что устройству может не хватать вычислительной мощности для быстрого выполнения JavaScript, что приводит к медленной работе приложения и заторможенности отклика. Это ведет к разочарованию пользователей и ухудшению пользовательского опыта. Если мы запустим этот JavaScript на серве­ ре и отправим клиенту минимум данных или разметки, клиентам с низким энерго­ потреблением не придется выполнять много работы, и, следовательно, пользова­ тельский опыт улучшится. В более широком смысле вопросы SEO и производительности в клиентских прило­ жениях с визуализацией подчеркивают важность соблюдения веб-стандартов и сле­ дования лучшим практикам. Они также подчеркивают необходимость рендеринга на стороне сервера или создания статического сайта в качестве более надежных альтернатив для предоставления контента эффективным и доступным способом, особенно для сайтов или приложений с большим количеством информации. Хотя расширенные функции рассматриваются как усовершенствование, принцип постепенного улучшения, при котором базовое содержимое и функциональность становятся доступными всем браузерам, хорошо сочетается с этими альтернатива­ ми. Предоставляя основной контент на стороне сервера, вы гарантируете, что все пользователи и поисковые системы будут иметь доступ к основному контенту и функциональности независимо от того, как выполняется тем клиентский JavaScript JavaScript в браузере. За­ может улучшить взаимодействие с пользователем, доба­ вив интерактивность, более насыщенную анимацию и другие расширенные воз­ можности браузеров и устройств, которые могут поддерживать этот контент. Нет смысла использовать только JavaScript на стороне клиента, поскольку это не явля­ ется главным предназначением Всемирной паутины. Роль JavaScript заключается в улучшении веб-страницы, а не в том, чтобы заменять собой веб-страницу. Рассмотрим следующий пример: i.rlpoгt React, { useEffect, useState} frOl'I "геасt"; const Номе = () => { const [data, setData] = useState([]); useEffect(() => { fetch("https://ap-t..exaмple.coм/data") .then((response) => response.json())
Глава 188 6 .then((data) => setData(data)); }, []); return ( <di.v> {data.мap((iteм) <di.v => ( key={iteм.id}>{iteм.title}</di.v> ))} </di.v> ); }; export default Номе; В этом примере мы извлекаем данные из API и отображаем их на стороне клиента. Мы можем сказать, что это клиентская сторона, потому что мы используем хук useEffect для извлечения данных и хук useState для сохранения данных useEffect выполняется только внутри браузера (клиента). в состоянии. Серьезным ограничением при этом является то, что некоторые поисковые роботы могут не увидеть данный контент, если мы не внедрим рендеринг на стороне серве­ ра. Существует риск того, что они увидят пустой экран или резервное сообщение (fallback message), что может привести к низкому рейтингу SEO. Другой распространенной проблемой клиентских приложений являются сетевые сбои, при которых начальная загрузка страницы блокируется из-за большого объе­ ма JavaScript-кoдa, который должен быть загружен, проанализирован и выполнен браузером, прежде чем веб-сайт или веб-приложение станут видимыми. В тех слу­ чаях, когда сетевое подключение является ограниченным ресурсом, это может при­ вести к тому, что веб-сайт или приложение полностью перестанут отвечать на за­ просы в течение значительного периода времени. В примере мы отправляем запрос на выборку данных во внешнюю конечную точку API (https://api.example.com/data) для получения некоторых данных. Эта выборка происходит после того, как наш исходный пакет JavaScript загружен, проанализи­ рован и выполнен, и все это происходит только после начальной загрузки НТМL­ кода страницы. Это и есть сетевой каскад, упомянутый выше, который приводит к снижению производительности. Визуализация этого процесса показана на рис. 6.1. Выглядит не очень ... С помощью рендеринга на стороне сервера мы можем добить­ ся большего успеха и позволить нашим пользователям сразу же увидеть полезный контент, изменив эту процедуру так, чтобы она имела следующий вид: Загрузить HTML (полный UI, с данными, полученными на стороне сервера) Действительно, первая загрузка уже содержит информацию, которая полезна поль­ зователю, потому что мы извлекли наши данные и отрисовали наш компонент на
Серверный React 189 сервере. Здесь нет "сетевого каскада", и пользователь получает всю свою информа­ цию немедленно. В этом ценность рендеринга на стороне сервера. Загрузить HTHL (пусту• страницу) . . . _ _. . . .► IЗагруэить JavaScript . . __ _ ....,► 1отобраэить начальный UI ..__ _ __..получить данные ..__..,_1{ Обновить UI) Рис. Начиная с версии 130,2 React 18, Запрос на выборку данных 6.1. размеры пакетов React и React DOM составляют Кбайт соответственно. Эти размеры указаны для последней версии 6,4 и React на момент написания книги и могут варьироваться в зависимости от версии и конфи­ гурации React, водственных JavaScript которую вы используете сегодня. Это означает, что даже в произ­ средах только для наши React пользователи (т. е. должны React + React DOM) загрузить около 136 Кбайт до загрузки, синтаксического анализа и выполнения остальной части кода нашего приложения. Это может при­ вести к замедлению начальной загрузки страницы, особенно на более медленных устройствах и в медленных сетях, и потенциально вызвать недовольство пользова­ телей. Более того, поскольку React, по сути, является владельцем DOM, и в клиент­ React, у наших пользова­ пока React и React DOM ских приложениях нет пользовательского интерфейса без телей не имеется другого выбора, кроме как ждать, загрузятся первыми, прежде чем это сделает остальная часть нашего приложения. Напротив, при серверном рендеринге приложение будет передавать отрисованный НТМL-код в потоковом режиме клиенту перед загрузкой любого JavaScript, что позволяет пользователям сразу же получать значимый контент. После отображения начальной страницы сервер загружает соответствующий JavaScript, возможно, пока пользователь все еще "вникает" в интерфейс с помощью процесса, называемого "гидратация". Подробнее об этом в следующих разделах. Первоначальная потоковая передача результатов рендеринга использование DOM с помощью JavaScript HTML и последующее позволяют пользователям быстрее на­ чать взаимодействие с приложением, что обеспечивает лучший пользовательский опыт. Приложение сразу становится доступным для пользователей, без необходи­ мости ждать загрузки каких-либо дополнительных функций, которые им могут по­ надобиться, а могут оказаться ненужными. Безопасность Клиентский рендеринг также может иметь проблемы с безопасностью, особенно при работе с конфиденциальными данными. Это связано с тем, что весь код прило­ жения загружается в браузер клиента, что делает его уязвимым для таких атак, как подделка межсайтовых запросов (cross-site request forgery, CSRF).
Глава 190 6 Не вдаваясь в подробности о CSRF, отметим, что распространенным способом борьбы с ним является контроль над сервером, который обслуживает веб-сайт или веб-приложение для ваших пользователей. Если у нас есть контроль над этим сер­ вером, он как надежный источник может отправлять клиенту соответствующие то­ кены защиты от CSRF, а затем клиент отправляет токены через форму или что-то подобное обратно на сервер, который может убедиться, что запрос поступает от правильного клиента. Это распространенный способ защиты от CSRF. Хотя технически возможно обслуживать клиентские приложения со статического сервера, который мы контролируем, и таким образом предотвращать CSRF, это все равно не лучший способ обслуживания веб-сайта в целом из-за других компромис­ сов, которые мы обсуждали до сих пор. Если у нас действительно есть контроль над сервером, то почему бы не добавить к нему SSR? В конечном счете вот что мы хотим сказать. ♦ Если у нас нет доступа к серверной части, но мы работаем в команде, где поль­ зуемся командой gi.t push1, предназначенной только для клиентского интерфей­ са, который затем волшебным образом где-то развертывается, то здесь возника­ ют риски, связанные с ♦ CSRF. Если у нас есть доступ к серверной части и наш веб-сайт или веб-приложение по-прежнему являются только клиентскими, мы уже можем достаточно эффек­ тивно бороться с ♦ CSRF, и риски безопасности, связанные с этим, исчезают. Если у нас есть доступ к серверной части и наш веб-сайт или веб-приложение по-прежнему являются клиентскими, существуют веские аргументы в пользу добавления рендеринга на стороне сервера, поскольку у нас есть доступ к серве­ ру, что позволяет использовать другие преимущества SEO и производительно­ сти, о которых мы уже говорили. Давайте немного разберемся с практической стороной вопроса, и для этого рас­ смотрим следующий пример: \./1\port React, { useState} frOl'I const Account = () => { coost [balance, setBalance] "геасt"; = use5tate(100); const handleWtthdrawal = async (arюunt) => { // Предположим, Чl'IO Эl'IOl'I запрос оl'lnравляеl'/Ся / / для обрабоl'lки вывода средсl'lв const response = awatt fetch("/wtthdгaw", { мethod: "POST", 1 gi.t на сервер push- одна из консольных команд Git. Она позволяет передать изменения из локального .git) в удаленный. Разработчики используют эту команду в репозитория (набора файлов из папки следующих целях: добавить новый функционал в основную ветку, исправить баг, закрыть уязвимость в коде и др. - Прим. пер.
Серверный React 1 191 headers: { "Content- Туре": "appli.cati.on/json", }, credenti.als: "i.nclude", body: JSON.stri.ngi.fy({ a~ount }), }); i.f (response.ok) { const updatedBalance = awai.t response.json(); setBalance(updatedBalance); } }; return ( <di.v> <h1>Account Balance: {balance}</h1> <button onCli.ck={() => handleWi.thdrawal(10)}>Wi.thdraw $10</button> <button onCli.ck={() => handleWi.thdrawal(S0)}>Wi.thdraw $50</button> <button onCli.ck={() => handleWi.thdrawal(100)}>Wi.thdraw $100</button> </di.v> ); }; ехрогt default Account; В этом коде функция handleWi.thdrawal отправляет РОSТ-запрос на гипотетическую /wi.thdrawal для обработки вывода средств. Риск CSRF серверную конечную точку может возникнуть, если эта конечная точка должным образом не подтвердит ис­ точник запроса и не потребует какой-либо формы токена, защищающего от CSRF. Злоумышленник может создать вредоносную (фейковую) веб-страницу, которая обманом заставит пользователя нажать на кнопку, и тем самым отправить РОSТ­ запрос конечной точке /wi.thdrawal от имени пользователя, что может в итоге при­ вести к несанкционированному снятию средств с учетной записи пользователя. Это связано с тем, что браузер автоматически включает в запрос файлы cookie, которые сервер использует для аутентификации пользователя. Если сервер не подтвердит происхождение запроса, он может быть обманным путем вовлечен в обработку за­ проса и отправку средства на счет злоумышленника. Если компонент отображается на стороне клиента, он может быть уязвим для атак CSRF, поскольку сервер и клиент не имеют общего секрета или контракта между собой. Говоря поэтично, клиент и сервер не знакомы друг с другом. Это может по­ зволить жения. злоумышленнику украсть средства или манипулировать данными прило­
192 Глава 6 Если бы мы использовали серверный рендеринг, мы могли бы устранить эти про­ блемы безопасности, отрисовав компонент на сервере с помощью специального секретного токена, сгенерированного сервером, а затем отправив НТМL-код, со­ держащий секретный токен, клиенту. Далее клиент возвратит этот токен на сервер, который его выдал, устанавливая безопасный двунаправленный контракт. Это по­ зволит серверу убедиться, что запрос поступает от правильного клиента, которого он предварительно авторизовал, а не от неизвестного человека, который, возможно, является злоумышленником. Развитие серверного рендеринга По этим причинам рендеринг на стороне сервера стал, пожалуй, лучшим методом повышения производительности и удобства работы с веб-приложениями. С помо­ щью серверного рендеринга приложения могут быть оптимизированы по скорости и доступности, что приводит к ускорению загрузки, улучшению SEO и повышению вовлеченности пользователей. Преимущества серверного рендеринга Давайте подробнее остановимся на преимуществах серверного рендеринга. Они должны стать понятны сразу же, как только мы разберемся с ограничениями кли­ ентского рендеринга. При серверном рендерuнге время на создание первой отрuсовкu страницы сокра­ щается. Это происходит потому, что сервер может визуализировать исходную НТМL­ разметку и отправлять ее клиенту, которая затем может быть немедленно ото­ бражена на экране. Это отличается от рендеринга на стороне клиента, когда кли­ ент должен JavaScript, дождаться загрузки, синтаксического анализа и выполнения прежде чем страница сможет быть отрисована. Серверный рендерuнг улучшает доступность веб-прuложений. Пользователи с медленным подключением к Интернету или устройствами с низ­ ким энергопотреблением могут получить лучший опыт, если они примут полно­ стью отрисованный НТМL-код вместо ожидания, когда JavaScript загрузит и отобразит страницу на стороне клиента. Серверный рендеринг может улучшить SEO веб-прuложений. Когда поисковые роботы индексируют ваш сайт, они могут видеть полностью отображенный HTML, что облегчает им понимание содержания и структуры вашего сайта. Серверный рендеринг может повысить безопасность веб-прuложенuй. Предоставляя основной контент на стороне сервера, вы гарантируете, что все пользователи и поисковые системы будут иметь доступ к основному контенту и
Серверный функциональности, независимо от выполнения ский JavaScript React 1 193 Кроме того, клиент­ JavaScript. может улучшить восприятие пользователем, добавив интерак­ тивность, более насыщенную анимацию и другие расширенные функции для браузеров и устройств, которые могут их поддерживать. Однако отображаемый сервером, является статичным и лишен интерактив­ HTML, ности, поскольку в него изначально не загружен какой-либо JavaScript. Он не со­ держит слушателей событий или другие динамические функции. Для того чтобы включить взаимодействие с пользователем и динамические функции, статический HTML должен быть "гидратирован", т. е. дополнен нужным JavaScript-кoдoм. Да­ вайте подробнее разберемся в концепции гидратации (hydration). Гидратация Гидратация это термин, используемый для описания процесса присоединения - слушателей событий и другой функциональности JavaScript к статическому который генерируется на сервере и отправляется клиенту. Цель гидратации HTML, - сде­ лать приложение, отображаемое сервером, полностью интерактивным после за­ грузки в браузер, что обеспечит пользователям быструю и приятную ("увлажнен­ ную") работу. В приложении React гидратация происходит после того, как клиент загружает при­ ложение, отображенное сервером. Затем выполняются следующие шаги. Загрузка клиентского пакета. Пока браузер отображает статический НТМL-код, он также загружает и анали­ зирует пакет код приложения. Этот пакет включает ком­ поненты необходимый для функционирования при­ JavaScript, содержащий React и любой другой код, ложения. Подключение слушателей событий. Как только пакет JavaScript загружен, React DOM слушателей подключая к элементам "гидратирует" статический HTML, событий и другие динамические функции. Обычно это делается с помощью функции hydrateRoot из react-dOl'l, ко­ торая использует корневой компонент React и DОМ-контейнер в качестве аргу­ ментов. Гидратация, по сути, преобразует статический НТМL-код в полностью интерактивное приложение React. После завершения процесса гидратации приложение становится полностью инте­ рактивным и может реагировать на ввод данных пользователем, извлекать данные и обновлять DOM по мере необходимости. DOM в статическом формате HTML со структурой, определенной компонентами React с помощью JSX. Крайне важно, чтобы структура, генерируемая компонентами React, соответствова­ ла структуре статического HTML. В случае несоответствия React не сможет кор­ ректно подключать слушателей событий и не будет знать, какой элемент React со- Во время гидратации React сопоставляет структуру элементов
Глава 194 6 поставляется с конкретным элементом DOM, что приводит к тому, что приложение работает не так, как ожидалось. Сочетая рендеринг на стороне сервера и гидратацию, разработчики могут создавать веб-приложения, которые быстро загружаются и обеспечивают беспрепятственное взаимодействие с пользователем. Считается ли вредной гидратация? Хотя гидратация~ отличный способ сделать отображенный сервером HTML ин­ терактивным, некоторые критикуют использование гидратации за то, что она вы­ полняется медленнее, чем хотелось бы, и часто ссылаются на возможность приме­ нения метода возобновления (resumaЬility)2 как на превосходную альтернативу (рис. 6.2). Давайте немного разберемся с этим. □ JavaScript Появляется быстрее JavaScr1pt HTML Рис. Отображение 6.2. Гидратация С помощью гидратации мы отрисовываем приложение React на сервере, а затем передаем результат клиенту. Однако на данный момент ничего интерактивного не происходит. Далее нашему браузеру необходимо загрузить клиентский пакет, под­ ключить слушателей событий и эффективно "перерисовать" страницу. Это большой объем работы, и иногда возникает задержка между появлением контента для поль­ зователя и тем моментом, когда пользователь действительно может начать взаимо­ действовать с сайтом. В противовес этому возобновление функционирует несколько иначе, как показано на рис. 6.3. С использованием возобновления все приложение отрисовывается на сервере и пе­ редается в браузер. Вместе с начальной разметкой все интерактивные действия се­ риализуются и также отправляются клиенту. После этого у клиента уже есть вся 2 При использовании техники resumability JavaScript-кoд частично отрисовывается на сервере, после чего конечное состояние отрисовки сериализуется и отправляется клиенту с соответствующей полезной НТМL-наrрузкой. Затем клиент завершает начатую на сервере отрисовку. тратя на это уже существенно меньше времени и ресурсов. - Прu.11. пер.
Серверный React 1 195 информация о том, как стать интерактивным по запросу и, таким образом, возобно­ вить работу с того места, где сервер остановился. Ему не нужно выполнять гидра­ тацию (т. е. подключать слушателей событий и отображать страницу на стороне клиента), но вместо этого он может десериализовать данные, предоставленные сер­ вером, и начать реагировать соответствующим образом. Отсутствие этапа гидрата­ ции может привести к более быстрому переходу на интерактивный режим interactive, (time to ТТI) и улучшению взаимодействия с пользователем. ll~lili■ Отображение JavaScr1pt Быстрее . ···············•, ,-·················· .,-················. :: -· -· .................... f Возможность возобновления работы ~....................................... , : , ........................................ ,' Дубликат удален Рис. 6.3. Возобновление Несмотря на то что возможность возобновления дает ощутимые преимущества, инженерное сообщество сомневается, что сложность реализации этого метода оп­ равдывает такие выгоды. Действительно, это более сложный подход, чем гидрата­ ция, и пока неясно, перевешивают ли преимущества затраты: да, время перехода к интерактивному режиму сокращается на несколько миллисекунд, но стоит ли так усложнять жизнь ради возможности возобновления? Это вопрос, который все еще обсуждается в сообществе React. Организация серверного рендеринга Если у вас уже есть клиентское приложение React, возможно, вам интересно, как добавить в него серверный рендеринг. К счастью, добавить серверный рендеринг в существующее приложение React относительно просто. Один из подходов заклю­ чается в использовании фреймворков серверного рендеринга, таких как Remix. Next.js или Эти платформы действительно являются лучшим способом рендеринга Rеасt-приложений на стороне сервера. Абстракции, применяемые этими фреймвор­ ками, могут вызвать у наиболее любопытных из нас желание понять базовые меха­ низмы, используемые для рендеринга на стороне сервера. Если вы человек с пытливым умом и вам интересно, как бы вы вручную могли до­ бавить серверный рендеринг в клиентское приложение React, или вам интересно
196 Глава 6 узнать, как это делают фреймворки, читайте дальше. Это материал, который вы, вероятно, не стали бы применять в производстве, а скорее использовать в образова­ тельных целях. Ручное добавление в клиентское Rеасt-приложение серверного рендеринга Если у вас есть клиентское приложение, вы можете добавить к нему рендеринг на стороне сервера следующим образом. Сначала вы создадите файл server js в корне вашего проекта. Этот файл будет со­ держать код для вашего сервера: // seгveг.js // Импорl'lируем нужные модули const express =requtre("expгess"); // Импорl'luруем библиоl'lеку Expгess.js // Импорl'lируем модуль Path для рабоРIЫ const path = requtre("path"); // Импорl'luруем библиОР1еку React const React =гequtre("react"); // Импорl'luруем ReactDOМSeгveг для серверного рендеринга const ReactDOМServer = requtre("react-dOP1/serveг"); // Импорl'lируем главный компоненl'I Арр const Арр = requtre("./src/App"); // Инициализируем приложение const арр = express(); из каР/QЛога sгс Expгess // Разворачиваем СРЮl'lические файлы из каl'lалога build app.use(express.stat'ic(path.jotn(_dtrna111e, "butld"))); // ОбрабаР1Ываем все СЕТ-запросы app.get("*", (геq, геs) => { // 01'/ображаем компоненf'IЫ Арр в HTML-cl'lpoкy const htмl =ReactDOМServer.renderToStrtng(<App />); // О,,,правляем res.send(' НТML-ol'/Bel'I, содержащий Оl'lображенный компоненl'I Арр <!DОСТУРЕ htмl> <htмl> <head> <tttle>Мy </head> React App</tttle> с nyl'lямu
Серверный React 1 197 <body> <!-- Внедрение отображенного компонента Арр --> <di.v i.d="root">${htмl}</di.v> <!-- Ссьmка на основной пакет JavaScri.pt --> <scri.pt src="/stati.c/js/мai.n.js"></scri.pt> </body> </htмl> .) ; }); // Запускаем сервер на nof)f'ly 3000 app.li.sten(3000, () => { // Выводим в консоль сообщение о console.log("Server li.steni.ng on }); В этом примере мы используем статические файлы из каталога запуске сервера рогt 3000"); Express для создания сервера, который обслуживает ./build, а затем отображает наше приложение React на сервере. Мы также используем ReactDOМServer, который преобразует наше при­ ложение React в НТМL-строку и затем вставляет ее в ответ, отправляемый клиенту. В этом сценарии мы предполагаем, что в нашем клиентском приложении какой-то скрипт, сохраняющий клиентский пакет JavaScript в каталог React есть build, на ко­ торый мы ссылаемся во фрагменте кода. Это важно для гидратации. Разобравшись со всеми этими деталями, давайте запустим наш сервер: node server.js Выполнение этой команды должно запустить наш сервер на порту консоль сообщение "Server li.steni.ng on рогt 3000 и вывести в 3000". Благодаря этим шагам у нас теперь есть приложение React с серверным рендерин­ гом. Если мы заглянем "под капот" серверного рендеринга, мы получим более глу­ бокое представление о том, как работает этот рендеринг и как он может принести пользу нашим приложениям React. Открыв браузер и перейдя по ссылке http://localhost:3000, мы увидим приложение, отображаемое сервером. Мы можем подтвердить, что это действительно серверное приложение, просмотрев исходный код этой страницы, который должен отобра­ жать реальную НТМL-разметку, а не пустой документ. Для полноты картины покажем, как должна выглядеть наша НТМL-разметка: <!DОСТУРЕ htl'll> <htrlt> <head> <titte>My React App</titte> </head>
Глава 198 6 <Ьоdу> <d'iv i.d="root"> <d'\.V> <h1>Hello, world!</h1> <p>Thi.s i.s а si.мple React арр.</р> </d'iv> </d'iv> <scr'ipt src="/stati.c/js/мai.n.js"></scri.pt> </Ьоdу> </htмl> Это НТМL-разметка, которая отправляется клиенту. Она содержит полностью от­ рисованный для нашего приложения HTML React, который может быть проиндек­ сирован поисковыми системами и который более доступен пользователям с мед­ ленным или ненадежным улучшению SEO подключением к Интернету. Это может и увеличит доступность нашего приложения привести к React. Гидратирование Поскольку вывод, отображаемый сервером, доступен пользователям, гидратация происходит тогда, когда мы загружаем наш клиентский пакет с тегом уже говорилось, гидратация других функций JavaScript <scri.pt>. Как это процесс присоединения слушателей событий и - к статическому HTML, который генерируется на сервере - сделать приложение, отображаемое и отправляется клиенту. Цель гидратации сервером, полностью интерактивным после загрузки в браузер. Если мы хотим внимательнее рассмотреть этап гидратации клиентского пакета на­ шего приложения, посмотрим на этот фрагмент кода: // Импорf'luруем нужные библиоf'lеки Vlport React frOP1 "react"; Vlport { hydrateRoot} frOP1 // Арр Vlport // - "react-doм/cli.ent"; главный компоненf'I нашего приложения Арр frOP1 "./Арр"; Гидраf'lируем Арр на cfllopoнe клиенf'lа hydrateRoot(docuмent, <Арр />); Благодаря серверному рендерингу и клиентской гидратации наше приложение пол­ ностью интерактивно и может реагировать на вводимую пользователем информа­ цию, извлекать данные и обновлять API для DOM по мере необходимости. серверного рендеринга в React В предыдущем разделе мы вручную добавили серверный рендеринг в клиентское приложение React, используя Express и ReactDOMServer. В частности, мы использова-
Серверный ли ReactDOМServer. renderToStri.ng() для рендеринга нашего React приложения 1 199 React в НТМL-строку. Это самый простой способ добавить сер верный рендеринг в прило­ жение API React. Однако есть и другие способы. Давайте более подробно рассмотрим серверного рендеринга Для начала разберем React и попробуем понять, когда и как их применять. API renderToStri.ng, его использование, преимущества и недос­ татки, а также случаи целесообразного применения в приложении React. В частно­ сти, давайте ответим на несколько вопросов: ♦ Что это такое? ♦ Как это работает? ♦ Как это вписывается в наше повседневное использование React? В первую очередь, давайте посмотрим, что это такое. renderToString renderToStri.ng - это API серверного рендеринга, предоставляемый позволяет вам визуализировать компонент Этот API работает синхронно и React React, который в виде НТМL-строки на сервере. возвращает полностью отрисованную НТМL­ строку, которая затем может быть отправлена клиенту в качестве ответа на запрос. renderToStri.ng обычно используется в серверных приложениях ния производительности, улучшения SEO React для увеличе­ и доступности. Использование Для того чтобы применить renderToStri.ng из пакета компонентом React renderToStri.ng, вам нужно импортировать функцию react-dof'1/server. Затем вы можете вызвать эту функцию с в качестве аргумента. В результате вы получите полностью от­ рисованный НТМL-код в виде строки. Вот пример использования отрисовки простого компонента React: 1.l'lport React frOl'I "react"; 1.l'lport { renderToStri.ng} frOl'I "react-dof'1/server"; funcnon Арр() { return ( <di.v> <h1>Hello, world!</h1> <p>Thi.s i.s а si.l'1ple React </di.v> арр.</р> ); } const htf'1l = renderToStri.ng(<App />); console.log(htf'1l); renderToStri.ng для
Глава 200 В этом 6 примере мы создаем простой компонент приложения и вызываем renderToStгlng, используя этот компонент в качестве аргумента. Функция возвраща­ ет полностью отрисованный НТМL-код, который можно отправить клиенту. Как это работает Эта функция просматривает дерево элементов ты DOM React, преобразует реальные элемен­ в строковое представление и, наконец, выводит это в строку. React <dt.v> преобразуется {}, "1-\ello, world!"); Здесь стоит напомнить, что в React.createEleмent("dt.v", в и в итоге получается: { type: "dt.v", ргорs: {}, chHdren: ["Нello, world! "] } Мы уже говорили об этом в предыдущих главах, но стоит напомнить еще раз здесь для дальнейшего обсуждения. По сути, JSX преобразуется в HTML по следующей схеме: JSX -> React.createEleмent -> React eleмent -> renderToStrt.ng как API является синхронным renderToStгt.ng(React eleмent) -> HTML и блокирующим, и это означает, что его нельзя прервать или приостановить. Если корневое дерево компонентов имеет многоуровневую глубину, оно может потребовать довольно большой обработки. Поскольку сервер обычно обслуживает несколько клиентов, renderToStrt.ng может быть вызван для каждого клиента, если только не предусмотрено какое-либо кеши­ рование, позволяющее быстро заблокировать цикл обработки событий и предот­ вратить перегрузку системы. В терминах кода renderToStrt.ng преобразует это: React.createEleмent( "sectt.on", { t.d: "Hst" }, React.createEleмent("hl", {}, "Tht.s t.s му Hst!"), React.createEleмent( "р", {}' "Isn't му Hst aмazt.ng? It contat.ns aмazt.ng tht.ngs!" ), React.cгeateEleмent( 11 ul 11 , {}' aмazt.ngTht.ngs.мap((t) ) ); => React.createEleмent("H", { key: t.t.d }, t.laЬel))
Серверный React 1 201 в следующий НТМL-код: <secti.on id="list"> <h1>This is му list!</h1> <p>Isn't му list aмazing? It contains <Ul> <lt>Thing l</lt> <lt>Thing 2</lt> <lt>Thing 3</lt> </ul> </secti.on> Поскольку React - things!</p> является декларативным, а элементы ными абстракциями, дерево во aмazing React React являются декларатив­ может быть преобразовано в любое другое дере­ в нашем случае дерево элементов React превращается в строковое представ­ ление дерева НТМL-элементов. Недостатки Хотя renderToString обладает рядом преимуществ, у него также есть некоторые не­ достатки. Производителыtость. Одним из основных недостатков API renderToString является то, что он может React. Поскольку он синхронный, работать медленно для больших приложений он способен заблокировать цикл обработки событий и сделать сервер невоспри­ имчивым. Это может стать особенно проблематичным, если ваше приложение имеет высокий трафик и большое количество одновременных пользователей. Кроме того, renderToString возвращает полностью отрисованную НТМL-строку, что может занимать много памяти в случае больших приложений. Это может привести к увеличению использования памяти на вашем сервере и потенциаль­ ному увеличению времени отклика, а также к "панике", которая приведет к ос­ тановке серверного процесса при большой нагрузке. Отсутствие поддержки потоковой передачи. renderToString не поддерживает потоковую передачу, а это означает, что необхо­ димо сгенерировать всю НТМL-строку целиком, прежде чем ее можно будет от­ править клиенту. Это может привести к замедлению передачи первого байта (time to first byte, TTFB) и отсрочке времени начала приема НТМL-кода клиен­ том. Данное ограничение может быть особенно проблематичным для больших приложений с большим количеством контента, поскольку клиент должен до­ ждаться создания всей НТМL-строки, прежде чем можно будет отобразить ка­ кой-либо контент. Для более крупных приложений или ситуаций, когда недостатки новятся критичными, React предлагает альтернативные API renderToString ста­ для рендеринга на сто­ роне сервера, такие как renderToPipeaЫeStreaм и renderToReadaЫeStreaм. Эти API воз-
Глава 202 6 вращают поток и поток браузера, соответственно, вместо полностью ото­ Node.js бражаемой НТМL-строки, что может обеспечить лучшую производительность при поддержке потоковой передачи. Мы рассмотрим это подробнее в следующем раз­ деле. renderToPipeaЬ/eStream renderToPtpeaЫeStreal'l - ленный в React 18. это интерфейс серверного рендеринга, впервые представ­ Он обеспечивает более эффективный и гибкий способ рендерин­ га больших приложений React щает можно поток, который с помощью потока направить в Node.js. объект Этот интерфейс возвра­ ответа (response object). renderToPtpeaЫeStreal'l обеспечивает больше контроля того, как происходит ренде­ ринг HTML, и позволяет достигнуть лучшей интеграции с другими потоками Кроме того, он полностью поддерживает конкурентные функции Suspense, React, Node.js. включая которые улучшают обработку асинхронной выборки данных во время рен­ деринга на стороне сервера. Поскольку это поток, он также доступен для передачи по сети, где фрагменты HTML могут быть асинхронно и кумулятивно отправлены клиентам по сети без какой-либо блокировки. Это приводит к более быстрому из­ менению TTFB и, как правило, повышению производительности. Для того чтобы переписать код нашего предыдущего сервера с использованием renderToPtpeaЫeStreal'l, мы могли бы сделать следующее: // seгveг.js const express = requtre("express"); const path = requtre("path"); const React = requtre("react"); const ReactDOМServer = requtre("react-dol'l/server"); const Арр = requtre("./src/App"); const арр = express(); app.use(expгess.stattc(path.jotn(_dtrnal'le, "butld"))); app.get{"*", (геq, геs) => { // Изменения начинаЮf'/СЯ Ot'IC/oдa const { ptpe} = ReactDOМServer.renderToPtpeaЫeStreal'l(<App />, { // Когда наше приложение гоР1ово к выборке данных ... onShellReady: () => { // соощаем клиенf'lу, чf'lo ~ посылаем HTML res.setHeader("Content-Type", "text/htl'll"); ptpe(res); // преобразуем выходные данные nof'loкa React в nof'loк гesponse
Серверный React 203 }, }); }); app.ltsten(3000, () => { console.log("Server Hstentng on port 3000"); }); Давайте углубимся в renderToPtpeaЫeStreaм, обсудим его функции, преимущества и варианты использования. Мы также предоставим фрагменты кода и примеры, кото­ рые помогут вам лучше понять, как реализовать этот API в ваших приложениях React. Как это работает Подобно renderToStrtng, дерево элементов React renderToPtpeaЫeStreaм использует декларативно описанное HTML, Node.js. Поток Node.js - это фундаментальная концеп­ Node.js, обеспечивающая эффективную обработку данных и вместо того, чтобы преобразовывать их в строку превращает дерево в поток ция в среде выполнения и манипулирование ими. Потоки предоставляют способ постепенной обработки данных по частям, вместо того чтобы загружать весь набор данных в память сразу. Этот подход особенно полезен, когда проводится работа с большими строками или потоками данных, которые не могут полностью поместиться в памяти или переда­ ваться по сети. Node.js-noтoки. По своей сути, поток Node.js представляет собой поток данных ме­ жду источником и получателем. Его можно рассматривать как конвейер, по кото­ рому передаются данные, сопровождаемые по пути различными операциями, при­ меняемыми для преобразования или обработки данных. Потоки Node.js делятся на четыре типа в зависимости от их характера и направле­ ния. Потоки, доступные для чтения. Доступный для чтения поток представляет собой источник данных, из которого вы можете считывать данные. Он генерирует такие события, как data, end и еггог. Примерами доступных для чтения потоков являются чтение данных из файла, получение данных из НТТР-запроса или генерация данных с помощью пользо­ вательского генератора. Функция React renderToPtpeaЫeStreaм возвращает поток для чтения, в котором вы можете считывать поток такой как объект HTML и выводить его в поток, доступный для записи, res (объект response из Express ). Потоки, доступные для записи. Поток, доступный для записи, представляет собой место назначения, куда вы можете записывать данные. Для того чтобы вы могли отправить данные в поток, он работает с такими методами, как wrHe() и end(). Потоки, доступные для запи-
204 Глава 6 си, генерируют такие события, как больше данных, и drai.n, когда получатель может обработать когда ошибка возникает во время записи. Примером по­ error, токов, доступных для записи, является объект res (объект response из Express). Двусторонние потоки. Двусторонний (дуплексный) поток представляет собой поток, одновременно доступный как для чтения, так и для записи. Он обеспечивает двунаправленный поток данных, и это означает, что вы можете как считывать из потока, так и за­ писывать в поток. Дуплексные потоки обычно используются для сетевых соке­ тов или каналов связи, где данные должны передаваться в обоих направлениях. Преобразующие потоки. Преобразующий поток - это особый тип двустороннего потока, который вы­ полняет преобразования данных во время прохождения данных через него. Он считывает входные данные, обрабатывает их и предоставляет обработанные данные в качестве выходных данных. Преобразующие потоки могут использо­ ваться для выполнения таких задач, как сжатие, шифрование, распаковка или анализ данных. Одной из мощных функций потоков Node.js является возможность конвейерной передачи данных между потоками. Такая передача данных позволяет подключать выходные данные потока для чтения непосредственно к входным данным потока, доступного для записи, создавая единый поток данных. Это значительно упрощает процесс обработки данных и сокращает использование памяти. Действительно, именно так работает потоковый рендеринг на стороне сервера в React. Потоки в Node.js handling). Проблема заключается в обратном давлении, которое представляет собой также поддерживают обработку обратного давления (backpressure накопление данных за буфером во время передачи данных. Когда поток, доступный для записи, не может обрабатывать данные достаточно быстро, поток, доступный для чтения, приостанавливает передачу данных, предотвращая потерю данных. Как только поток, доступный для записи, готов потреблять больше данных, он генери­ рует событие drai.n, сигнализирующее о возможности возобновления передачи дан­ ных в читаемый поток. Если не углубляться в подробности и не отвлекаться от темы, потоки Node.js пред­ ставляют собой мощную абстракцию для масштабируемой обработки данных и экономии памяти. Разбивая данные на управляемые блоки и обеспечивая поэтап­ ную обработку, потоки гарантируют эффективную обработку больших массивов данных, операции ввода-вывода файлов, сетевую связь и многое другое. renderToPipeaЫeStream. В React целью потоковой передачи компонентов в доступ­ TTFB приложений, отображаемых сер­ ный для записи поток является уменьшение вером. Вместо того чтобы ждать, пока будет сгенерирована вся НТМL-разметка перед отправкой клиенту, эти методы позволяют серверу начинать отправку фраг­ ментов НТМL-ответа по мере их готовности, что сокращает общую задержку.
Серверный React 205 Функция renderToPi.peaЫeStreal'1 является частью серверного средства визуализации (рендерера) ложения рера под React, которое разработано для поддержки потокового рендеринга при­ React в формате потока Node.js. Это часть серверной архитектуры ренде­ названием "Fizz". ~ Мы собираемся очень глубоко погрузиться в детали реализации React, ко­ торые могут меняться с течением времени. Еще раз подчеркну, что это сделано в образовательных целях и для удовлетворения любопытства чи­ тателя. Возможно, этот материал не совсем соответствует деталям реали­ зации React на момент прочтения, но достаточно важен, чтобы было воз­ можно получить хорошее представление о том, как это работает на момент написания книги. Этот материал, вероятно, не является чем-то таким, что вы бы использовали в производстве, и не имеет существенного значения для понимания того, как использовать React, но предназначен только для обучения и удовлетворения любопытства. Не слишком отвлекаясь от нашего контекста рендеринга на стороне сервера, по­ смотрим на упрощенную схему того, как он работает. Создание запроса. Функция renderToPi.peaЫeStreal'1 принимает в качестве входных данных элементы React, которые должны быть отображены, и дополнительный объект тем она создает объект запроса с помощью функции ект запроса инкапсулирует элементы React, options. За­ createRequestll'1pl. Этот объ­ ресурсы, состояние ответа и кон­ текст формата. Запуск работы. После создания запроса вызывается функция startWork с запросом в качестве ар­ гумента. Она инициирует процесс рендеринга. Процесс рендеринга является асинхронным и может быть приостановлен и возобновлен по мере необходимо­ сти, для чего и нужен Suspense React Suspense. Если компонент заключен в границу и инициирует некоторую асинхронную операцию (например, выборку данных), рендеринг этого компонента (и, возможно, его аналогов) может быть "приостановлен" ("suspended") до завершения операции. Пока рендеринг компонента приостановлен, он может отображаться в "резерв­ ном" ("fallback") состоянии, которое обычно представляет собой индикатор за­ грузки или другой заполнитель. Как только операция завершается, компонент "возобновляется" и отображается в своем конечном состоянии. мощная функция, которая позволяет React Suspense - это более эффективно обрабатывать асинхронную выборку данных и ленивую (отложенную) загрузку во время рен­ деринга на стороне сервера. Преимущества заключаются в том, что мы можем сразу предоставить пользова­ телю значимую страницу, а затем постепенно дополнять ее новыми данными по мере их поступления. Это мощный метод, который может быть использован для улучшения взаимодействия пользователей с приложениями React.
Глава 206 6 Возвращение конвейерного потока. renderToPi.peaЫeStreaГ'1 затем возвращает объект, который включает в себя метод pi.pe и метод аЬогt. Метод pi.pe используется для передачи отображаемых выход­ ных данных в доступный для записи поток (например, объект НТТР Node.js). response в Метод аЬогt можно использовать для отмены любого ожидающего вво­ да-вывода и перевода всего, что осталось, в режим отображения на клиенте. Передача данных по конвейеру в точку назначения. Когда метод pi.pe вызывается с потоком, который отправляется по назначению, он проверяет, начали ли данные уже передаваться. Если нет, он устанавливает значение hasStartedFlowlng равным true и вызывает функцию startFlowi.ng с за­ просом и точкой назначения. Он также настраивает обработчики событий пото­ ка drai.n, еггог и close. Обработка событий потока. Обработчик событий drai.n запускает функцию startFlowi.ng, чтобы возобновить поток данных, когда целевой поток будет готов к приему дополнительной ин­ формации. Обработчики событий еггог и close вызывают функцию аЬогt, чтобы остановить процесс рендеринга, если в целевом потоке возникает ошибка или поток закрывается преждевременно. Прерывание рендеринга. Метод аЬогt для возвращаемого объекта может быть вызван с указанием причи­ ны остановки процесса рендеринга. Он вызывает функцию аЬогt из модуля react-server с запросом и причиной. Фактическая реализация этих функций требует более сложной логики для обработ­ ки таких процессов, как прогрессивный рендеринг, обработка ошибок и интеграция с остальными средствами серверного рендеринга можно найти в пакетах react-server React. Код для этих функций и геасt-dоГ'1 исходного кода React. Особенности renderToPipeaЬ/eStream Перечислим особенности renderToPi.peableStreaГ'1. Потоковая передача. renderToPi.peaЫeStreaГ'1 возвращает передаваемый конвейером поток торый может быть передан в объект response. Node.js, ко­ Это позволяет серверу начать от­ правку НТМL-кода клиенту до того, как будет отображена вся страница, что обеспечивает более быстрое взаимодействие с пользователем и более высокую производительность для больших приложений. Гибкость. renderToPi.peaЫeStreaГ'1 обеспечивает больший контроль над тем, как отображает­ ся НТМL-код. Он может быть легко интегрирован с другими потоками Node.js, что позволяет разработчикам настраивать конвейер рендеринга и создавать бо­ лее эффективные решения для рендеринга на стороне сервера.
Серверный Поддержка React 207 Suspense. renderT0Pi.peaЫeStrea111 полностью поддерживает конкурентные функции включая 1 Suspense. React, Это позволяет разработчикам во время рендеринга на стороне сервера более эффективно управлять асинхронной выборкой данных и отложен­ ной загрузкой, гарантируя, что зависимые от данных компоненты будут отобра­ жаться только после того, как будет доступна необходимая информация. Как это работает Давайте рассмотрим некоторый код, иллюстрирующий преимущества этого API. У нас есть приложение, которое отображает список пород собак. Список заполняется путем извлечения данных из конечной точки API. Приложение визуализируется на сервере с помощью гenderT0Pi.peaЫeStrea111, а затем отправляется клиенту. Давайте начнем с рассмотрения нашего компонента // - списка пород: ./sгc/DogBгeeds.jsx const dogResource = createResource( fetch("https://dog.ceo/api./breeds/li.st/all") .then((r) => r.json()) .then((r) => Object.keys(r.111essage)) ); functi.on DogBreeds() { гetuгn ( <Ut> <Suspense fallback="Loadi.ng ... "> {dogResource.read().111ap((pгofi.le) => ( <ti. key={profi.le}>{profi.le}</ti.> ))} </Suspense> </ut> ); } ехрогt defautt DogBreeds; Теперь посмотрим на наше приложение Арр, которое содержит компонент // sгc/App.js ;,,,,рогt React, { Suspense} frOl'I "геасt"; const Li.stOfBreeds = React.lazy(() => functi.on гetuгn Арр() ( { tl'1poгt("./DogBreeds")); DogBreeds:
208 Глава 6 <di.v> <h1>Dog Breeds</h1> <Suspense fallback={<di.V>Loading Dog Breeds ... </di.v>}> /> <Li.stOfВгeeds </Suspense> </di.v> ); } ехрогt default Арр; Обратите внимание, мы используем главах. Мы имеем здесь React. lazy, как упоминалось в предыдущих Suspense для демонстрации того, как Suspense. Хорошо, давайте свяжем все это вме­ границу renderT0PipeaЫeStrearr1 обрабатывает сте с помощью Express-cepвepa: // server.js 'U'lport express frOPI express 'U'lport React frOPI 11 геасt 11 ; 'U'lport { renderT0PipeaЫeStrearr1} frOPI react-dorr1/server 'U'lport Арр frOPI Арр. j SX 11 11 ; 11 11 const 11 • / 11 ; ; = express(); арр app.use(express.stati.c( build 11 11 ) ) ; app.get( async (req, res) => { // Определяем исходную НТМL -cf'lpyкf'lypy const htrr1lStart =' <!DОСТУРЕ htrr1l> <htrr1l lang= en > <head> <rr1eta charset= UTF-8 /> <PJeta narr1e= viewport content= width=device-width, inHial-scale=l.0" /> <title>React Suspense with renderT0PipeaЫeStrearr1</title> </head> <body> <div id= root > 11 / 11 , 11 11 11 11 11 11 11 11 11 // Записываем начальный HTML res.write(htrr1lStart); в response
Серверный // // React 209 Вызываем гendeгToPipeaЬleStгeaf'/ вмесf'/е с Rеасt-компоненf'lом приложения и объекf'lом options const { pipe} = для обрабоf'lки гof'/oвнocf'lu оболочки renderToPipeaЫeStreaм(<App onShellReady: () => { // Передаем ОГ'lрuсованный pipe(res); />, { вывод в nof'loк гesponse, когда оболочка гоf'lова }, }); }); // Запускаем сервер на nopf'ly 3000 и выводим cooбllf!нue app.listen(3000, () => { console.log("Server is Hstening оп port 3000"); }); В этом фрагменте кода мы отвечаем на запрос потоком НТМL-кода. Мы использу­ ем renderToPipeab leStreaм для отображения компонента нашего приложения в виде потока, а затем передаем этот поток в наш объект будет готова, мы используем опцию onShellReady response. Как только оболочка для передачи потока в объект response. Оболочка - это НТМL-код, который отображается до того, как приложе­ ние React будет гидратировано, и до того, как будут разрешены зависимости дан­ это ных, заключенные в границы ожидания Suspense. В нашем случае оболочка HTML, который отображается до того, как породы собак будут извлечены из API. Давайте посмотрим, что произойдет, когда мы запустим этот код. Если мы посетим адрес http://localhost:3000, то попадем на страницу с заголовком "Dog Breeds" ("Породы собак") и увидим надпись ожидания (Suspense fallback) "Loading Dog Breeds ... " ("Загрузка пород собак ... "). Это оболочка, которая визуали­ зируется перед тем, как породы собак будут извлечены из API. По-настоящему кру­ то то, что даже если мы не включим React на стороне клиента в наш HTML и не гидратируем страницу, надпись ожидания будет заменена списком пород собак, как только он будет получен из API. Эта замена DOM, когда данные становятся дос­ тупными, происходит полностью на стороне сервера, без использования React на стороне клиента! Давайте разберемся, как это работает, немного подробнее. ~ И снова мы собираемся углубиться в детали реализации React, которые, скорее всего, со временем претерпят изменения. Цель этого упражнения (и всей книги) не в том, чтобы зацикливаться на отдельных деталях реализа­ ции, а в том, чтобы понять лежащий в их основе механизм, чтобы мы мог­ ли лучше изучить тельно для React и свободнее в нем ориентироваться. Это не обяза­ испо.1ыокания React, но понимание основ может дать нам подсказки и практические инструменты для применения в нашей повсе­ дневной работе с React. После этого замечания давайте двигаться дальше.
Глава 210 6 Когда мы зайдем на http://localhost:3000, сервер ответит НТМL-оболочкой, кото­ рая включает заголовок "Породы собак" и надпись ожидания "Загрузка пород со­ бак ... " НТМL-код страницы выглядит следующим образом: <!DОСТУРЕ htмl> <h"tP!l lang="en"> <head> <rieta charset="UTF-8" /> <rieta naмe="vi.ewport" content="wi.dth=devi.ce-wi.dth, i.ni.ti.al-scale=l.0" /> <ti.tle>React Suspense wi.th renderToPi.peaЫeStreaм</tttle> </head> <Ьоdу> <di.v i.d="root"> <di.v> <h1>User Profi.les</h1> <!--$?--><teriplate i.d="B:0"></teriplate> <di.v>Loadi.ng user profi.les ... </di.v> <!- -/$- -> </di.v> <di.v hi.dden i.d="S:0"> <Ul> <!--$--> <li.>affenpi.nscher</li.> <li.>afri.can</li.> <li.>ai.redale</li.> [ ... ] <!- -/$- -> </ul> </di.v> <SCГi.pt> functton $RC(a, Ь) { а= docuмent.getEleмentByld(a); = docuмent.getEleмentByld(b); Ь b.parentNode.reмoveChi.ld(b); i.f { (а) а= a.previ.ousSi.Ыi.ng; vаг f = a.parentNode, с= a.nextSi.Ыi.ng, е = 0; do { i.f (с vаг && 8 === c.nodeType) { d = c.data;
Серверный React 1 211 tf ("/$" === d) i.f (0 === е) break; el.se е- - ; el.se ("$" !== d && "$?" !== d && "$!" !== d) 11 е++; } d = c.nextSiЫing; f.rel'ЮveChild(c); с= d; } whi.1.e (с); for (; b.firstChild; ) f.insertBefore(b.firstChi.ld, a.data = "$"; a._reactRetгy && a._reactRetry(); с); } } $RC("B:0", "5:0"); </scri.pt> </di.v> </Ьоdу> </ht:1'11.> То, что мы видим, довольно интересно. Здесь есть элемент ванным идентификатором ID (в данном случае В:0) <tel'1plate> и со сгенериро­ несколько НТМL­ комментариев. НТМL-комментарии используются для обозначения начала и конца оболочки. Это маркеры или "границы дыр", куда будут помещаться полученные данные, как только ожидание HTML Suspense будет закончено. Элементы лов без введения дополнительного уровня "обертывания" хии <ter,plate> в предоставляют способ построения поддеревьев документа и поддержки уз­ DOM. (wrapping level) в иерар­ Они служат в качестве облегченных контейнеров для управления груп­ пами узлов, повышая производительность за счет сокращения объема работы, выполняемой при манипулировании DOM. <script>. Этот тег <scгi.pt> содержит функцию с именем $RC, которая используется для замены оболочки фактическим содержимым. Функция $RC принимает два аргумента: идентификатор элемента <tel'1plate>, который содер­ жит маркер, и идентификатор элемента <div>, который содержит резервную над­ Также у нас есть элемент пись. Затем функция заполняет маркер отображаемым пользовательским интерфей­ сом после того, как данные будут доступны, при этом замещая надпись ожидания. Очень жаль, что эта функция минимизирована, но давайте попробуем "укрупнить" ее, чтобы понять, что она делает. Если мы это сделаем, то увидим следующее: functi.on reactCOl'lponentCleanup(reactMarkerld, si.Ыi.ngld) { let геасtМагkег = docuf'lent.getElef'lentByid(гeactMarkerid); 1.et siЫing = docuf'lent.getElef'lentByid(si.Ыi.ngid); si.Ыi.ng.parentNode.rel'ЮveChi.ld(si.Ыi.ng);
Глава 212 6 i.f (reactMarker) { reactMarker = reactмarker.previ.ousSi.Ыi.ng; let parentNode = reactMarker.parentNode, nextSi.Ыi.ng = reactMarker.nextSi.Ыi.ng, nestedlevel = 0; do { i.f (nextSi.Ыi.ng && 8 === nextSi.Ыi.ng.nodeType) { let nodeData = nextSi.Ыi.ng.data; i.f ("/$" === nodeData) { i.f (0 === nestedlevel) { Ьгеаk; } else { nestedlevel--; } } else i.f ("$" !== nodeData && "$?" !== nodeData && "$!" !== nodeData) { nestedlevel++; } } let nextNode = nextSi.Ыi.ng.nextSi.Ыi.ng; parentNode.rel'lOveChi.ld(nextSi.Ыi.ng); nextSi.Ыi.ng } whi.le whi.le = nextNode; (nextSi.Ыi.ng); (si.Ыi.ng.fi.rstChi.ld) { parentNode.i.nsertBefore(si.Ыi.ng.fi.rstChi.ld, nextSi.Ыi.ng); } reactмarker.data = "$"; reactMarker._reactRetry && reactMarker._reactRetry(); } } reactCOfТlponentCleanup("B:0", "5:0"); Давайте разберем это подробнее. Функция принимает два аргумента: reactMarkerid и si.bl i.ng!d. По сути, маркер - это область, куда будут помещаться отображаемые компоненты, как только они станут доступны, а si.bltng - это запасной вариант для ожидания (Suspense fallback). Затем, когда данные становятся доступными, функция удаляет элемент si.Ыi.ng из DOM, используя метод rel'lOveChi.ld его родительского узла.
Серверный Функция запускается, если существует элемент Она присваивает текущему React 213 reactMarker. reactMarker предыдущее значение reactMarker. Функция parentNode, nextSi.Ыi.ng и nestedlevel. также инициализирует переменные Цикл do ... whi.le используется для обхода дерева DOM, начиная с элемента nextSi.Ы i.ng. Цикл продолжается до тех пор, пока существует элемент nextSi.Ы i.ng. Внутри цикла функция проверяет, является ли элемент nextSi.Ы i.ng узлом коммента­ рия (обозначается значением nodeType, равным 8). ♦ Если элемент nextSi.Ы i.ng является узлом комментария, функция проверяет его данные (т. е. текстовое содержимое комментария). Она проверяет, равны ли данные "/$", nestedlevel что равно означает конец вложенной структуры. Если значение 0, цикл прерывается, указывая на то, что желаемый конец nestedlevel не равно 0, это означает, что те­ структуры достигнут. Если значение кущий узел комментария"/$" является частью вложенной структуры, и значение nestedleve l ♦ уменьшается. Если данные узла комментария не равны"/$", функция проверяет эти данные на равенство ной "$", "$?" структуры. nestedleve l или Если "$! ". Эти значения указывают на начало новой вложен­ встречается какое-либо из этих значений, Во время каждой итерации цикла элемент nextSi.Ыi.ng (т. е. граница ляется из величина увеличивается. DOM с помощью метода Suspense) уда­ re111oveChi.ld на его родительском узле. Цикл про­ должается со следующим одноуровневым элементом дерева DOM. После завершения цикла функция перемещает все дочерние элементы одноуровне­ вого элемента в местоположение непосредственно перед следующим одноуровне­ вым элементом в дереве Этот процесс эффек­ тивно заменяет резервный перестраивает вариант Suspense DOM, используя метод i.nsertBefore. DOM вокруг элемента reactMarker и компонентом, который он обертывает. Затем функция присваивает данным элемента reactMarker значение "$", которое ис­ пользуется для пометки компонента с целью последующей обработки или ссылки на него. Если в элементе reactMarker существует свойство reactRetry и это функция, функция вызывает данный метод. Если что-то из этого оказалось трудным для понимания, не беспокойтесь об этом. Мы можем обобщить все это здесь: по сути, эта функция ожидает, пока зависящие от данных компоненты React будут готовы, и после этого заменяет резервные вари­ анты компонентами, отображаемыми сервером. Функция использует узлы коммен­ тариев с конкретными значениями данных для определения структуры компонен­ тов и соответствующим образом манипулирует DOM. Поскольку это встроено в наш НТМL-код, посылаемый сервером, мы можем передавать данные потоком по­ добным образом с помощью renderT0Pi.peaЫeStrea111 и заставлять браузер отображать пользовательский интерфейс по мере его доступности, даже не включая React в па­ кет браузера и не применяя гидратацию. Таким образом, при рендеринге на стороне сервера renderT0Pi.peaЫeStrea111 дает нам гораздо больше контроля и возможностей по сравнению с renderToStri.ng.
Глава 214 6 renderToReadaЬ/eStream Предыдущий ток Node.js API, который мы рассмотрели, renderToPi.peaЫeStreal'1, использует по­ "под капотом". Однако браузеры также поддерживают потоки, и потоки браузера немного отличаются от потоков Node.js. Потоки Node.js в первую очередь предназначены для работы в серверной среде, где они имеют дело с файловым вво­ дом-выводом, сетевым вводом-выводом или любым видом сквозной потоковой пе­ редачи. Они используют пользовательский в среде долгое время были основной частью имеют различные API, определенный Node.js. Nоdе.js-потоки Node.js, и классы для чтения, записи, двустороннего доступа и преобразования потоков и ис­ пользуют такие события, как data, end и error для управления потоком и обработки данных. Браузерные потоки предназначены для работы в клиентской среде внутри веб­ браузеров. Они часто имеют дело с потоковой передачей данных при сетевых за­ просах, потоковой передачей мультимедиа или другими задачами браузерной обра­ ботки данных. Браузерные потоки соответствуют Hypertext Application Technology Working Group), стандарту WHATWG (Web целью которого является стан­ дартизация АРI-интерфейсов в Интернете. В отличие от Nоdе.js-потоков, потоки браузера для управления и последующей обработки данных используют такие ме­ тоды, как read(), wri.te() и pi.peThrough(). Они предоставляют более стандартизиро­ (promise-based) API. Вот пример потока, кото­ ванный и основанный на обещаниях рый можно прочитать в среде браузера: const readaЫeStreal'1 = new ReadaЫeStrea1'1({ start(controller) { controller.enqueue("Hello, "); controller.enqueue("world!"); controller.close(); }, }); const reader = readaЫeStreal'1.getReader(); async functi.on readAllChunks(streal'1Reader) { let result = ' whi.le (true) { const { done, value} = awai.t streal'1Reader.read(); i.f (done) { 1111. Ьгеаk; } result += value; } гetuгn } result;
Серверный React 1 215 readAllChunks(reader).then((text) => { console.log(text); }); Хотя потоки Node.js и браузеров предназначены для обработки потоковых данных, они работают в разных средах и при этом их API и стандарты немного различают­ ся. Nоdе.js-потоки управляются событиями и хорошо подходят для работы на сто­ роне сервера, в то время как потоки браузера основаны на обещаниях, соответст­ вуют современным веб-стандартам и адаптированы для работы на стороне клиента. Для поддержки обеих сред в React есть renderToPi.peaЫeStreal"l для Nоdе.js-потоков и renderToReadaЫeStreal"l для потоков браузера. API renderToReadaЫeStreal"l похож на renderToPi.peaЫeStreal"l, но он возвращает поток, доступный для чтения браузерами, вместо потока, созданного на основе Когда какой API Node.js. использовать? renderToStri.ng не идеален, потому что он синхронный. Это вызывает проблемы по ряду причин. Сетевой ввод-вывод является асинхронным. Любая выполняемая нами выборка данных зависит от получения данных из ка­ кого-либо источника: из базы данных, веб-службы, файловой системы и т. д. Эти операции часто являются асинхронными: это означает, что в различных случаях они начинаются и заканчиваются в разные моменты времени. Посколь­ ку функция renderToStri.ng является синхронной, она не может ждать завершения асинхронных запросов и должна немедленно отправить строку в браузер. Это означает, что из-за того, что сервер не может завершить работу, клиент получает оболочку (shell) до загрузки каких-либо данных, и в идеале клиент продолжает работу с того места, где сервер остановился после завершения гидратации. Это приводит к проблемам с производительностью из-за сетевых сбоев. Серверы одновременно обслуживают много клиентов. Если ваш сервер, вызывающий renderToStri.ng, занят рендерингом в строку и 30 клиентов отправили ему новые запросы, этим новым клиентам придется по­ дождать, пока сервер завершит свою текущую работу. Поскольку renderToStri.ng выполняется синхронно, он блокируется до тех пор, пока не будет завершен. В отношениях "один ко многим" между сервером и клиентами это блокирование означает, что ваши клиенты ждут дольше, чем могли бы. Более новые альтернативы, такие как renderToPi.peaЫeStгeal"l и renderToReadaЫeStreal"l, представляют собой асинхронные потоковые подходы, которые решают обе эти проблемы, при этом renderToPi.peaЫeStreal"l - "какой API лучше renderToReadaЫeStreal"l является нативным для браузера, для всего сервера. Таким использовать образом, на сервере?", если то задаться ответом а вопросом будет либо renderToPi.peaЫeStreal"l, либо renderToReadaЫeStreal"l, в зависимости от среды.
216 Глава 6 Тем не менее, хотя renderTo*Strea111 представляет собой превосходный набор АРI­ интерфейсов, на момент подготовки этой книги не была еще написана "полная пользовательская история" этих API. Многие сторонние библиотеки, которые дос­ тупны в настоящее время, не будут работать с этими функциями, особенно если речь идет о библиотеках для извлечения данных или концептуально требуется "полный запуск" CSS. Это связано с тем, что им ("full run") на сервере, затем необходимо создать данные, а далее повторно отобразить приложение с этими данными для на­ чала фактической потоковой передачи с сервера клиенту. Они не поддерживают сценарии, в которых приложение еще не завершило рендеринг на сервере, но должно начать частично обновляться в браузере. Это проблема React: в React 18 (последней версии на момент написания книги) от­ сутствуют АРI-интерфейсы, которые позволяли бы поддерживать любой вид пото­ ковой передачи или частичной регидратации (т. е. повторной гидратации) сторон­ них данных. Команда таких как React недавно добавила в prefetchDNS, preconnect, preload react-do111 несколько новых API, и т. д., чтобы решить эту проблему, но эти инструменты будут поставляться только с React 19. Даже с этими АРI-интерфей­ сами все еще не хватает нескольких важных АРI-интерфейсов, чтобы сделать renderT0Pi.peaЫeStrea111 полностью жизнеспособной технологией. Единственным действительно жизнеспособным вариантом использования renderT0Pi.peaЫeStrea111 прямо сейчас была бы предварительная выборка всех необ­ ходимых данных (или, в случае библиотеки помощью renderToStri.ng для CSS, рендеринг всего приложения с "предварительной записи" всех отображаемых классов) перед вызовом renderT0Pi.peaЫeStrea111. Однако такой подход в значительной степени устранил бы большинство его преимуществ перед вратил бы его в синхронный renderToStri.ng, т. е. по сути, пре­ API. Учитывая все обстоятельства, это сложные задачи, которые требуют тщательного планирования и дальнейшего рассмотрения вопроса о том, какие API следует при­ менять, что в равной степени зависит от ваших текущих проектов и вариантов ис­ пользования. Таким образом, ответ на вопрос в названии этого раздела звучит так: "это зависит ... " или "просто молча используйте фреймворк" и предоставьте реше­ ние более широкому сообществу. Стоит ли изобретать велосипед? Создание пользовательской реализации серверного рендеринга для приложения React торые может быть сложной и трудоемкой задачей. Хотя API React предоставляет неко­ для серверного рендеринга, создание пользовательского решения с нуля может привести к различным проблемам и неэффективному использованию рабо­ чего времени. В этом разделе мы рассмотрим причины, по которым лучше пола­ гаться на устоявшиеся фреймворки, такие как венное серверное решение для рендеринга. Next.js и Remix, а не создавать собст­
Серверный React 1 217 Обработка пограничных случаев и сложностей. Приложения React могут стать довольно сложными, а реализация серверного рендеринга требует решения различных пограничных случаев (edge cases) и дру­ гого рода сложностей. Это может включать в себя обработку асинхронной вы­ борки данных, фрагментацию кода и управление различными событиями жиз­ ненного цикла React. Используя фреймворк, подобный Next.js или Remix, вы можете избежать необходимости самостоятельно бороться с этими трудностями, поскольку эти фреймворки имеют встроенные решения для многих распростра­ ненных проблемных ситуаций. Одной из таких проблем является безопасность. Поскольку сервер обрабатывает многочисленные запросы клиентов, крайне важно обеспечить, чтобы конфиден­ циальные данные от одного клиента случайно не попали к другому. Именно здесь такие платформы, как Next.js, Remix и Gatsby, могут оказать неоценимую помощь. Представьте себе ситуацию, когда клиент А обращается к серверу, и его данные кешируются сервером. Если сервер случайно передаст эти кеширо­ ванные данные клиенту В, может быть раскрыта конфиденциальная информация. Рассмотрим следующий пример: seгveг.js // // Импорrr1ируем модуль expгess const express = requi.re("express"); // Создаем новый ехргеss-экземмяр const арр = express(); приложения // Создаем переменную, хранящую кешированные данные пользовамеля. // Изначально эмо null, м. к. кешированных данных пока нем let cachedUserData = null; // Определяем обрабОf'lчuк для СЕТ-запросов к "/useг/:useгld". // Получим омвем с данными пользовамеля useгid app.get("/user/:userld", (req, res) => { // Извлекаем useгld из парамемров запроса const { userld} = req.paraмs; // Проверяем, есмь ли кешированные данные пользовамеля. // Если да, омправляем эми данные i.f (cachedUserData) { return res.json(cachedUserData); } // // Если нем, извлекаем данные пользовамеля из базы данных или другого исмочника.
Глава 218 6 // ПредполагаеfТIСЯ, чмо функция fetchUserData const userData = fetchUserData(userld); // Обновляем кеш извлеченными cachedUserData = userData; // Омправляем извлеченные res.json(userData); определена ранее данными данные пользовамеля }); // Запускаем сервер, слушаем порм 3000. // Выводим в консоль сообщение о гомовноСl'/u сервера app.listen(3000, () => { console.log("Server listening on port 3000"); }); В приведенном коде cachedUserData предназначен для кеширования пользова­ тельских данных, но он используется совместно для всех запросов независимо от идентификатора пользователя /user /: userld, сервер userld. Каждый раз, когда выполняется запрос к проверяет cachedUserData на наличие кешированных данных. Если они есть, он возвращает кешированные данные независимо от того, совпа­ дает ли идентификатор текущего пользователя с идентификатором пользователя из кешированных данных. Если нет, он извлекает данные, кеширует их и воз­ вращает. Это означает, что если последовательно выполнить два запроса к /user /1 /user /2, и то второй запрос получит данные первого пользователя, что является существенной проблемой безопасности. Более безопасной стратегией кеширования было бы кеширование данных таким образом, чтобы они были связаны с идентификатором пользователя, т. е. чтобы у каждого пользователя был собственный кеш. Один из способов сделать это - использовать объект для хранения кешированных данных с идентификатором пользователя в качестве ключа. Если мы "выкатываем" свои собственные решения, всегда существует риск че­ ловеческой ошибки. Если мы опираемся на фреймворки, созданные крупными сообществами, этот риск снижается. Такие платформы разработаны с учетом требований безопасности и обеспечивают правильную обработку конфиденци­ альных данных. Они предотвращают возможные сценарии утечки данных, ис­ пользуя безопасные и изолированные методы извлечения данных. Оптимизация производителыюсти. Фреймворки поставляются с множеством готовых решений для оптимизации производительности. Эти оптимизации могут включать автоматическую фраг-
Серверный React 219 ментацию кода, рендеринг на сервере и кеширование. Создание пользователь­ ского решения для серверного рендеринга может не включать эти оптимизации по умолчанию, и их реализация может оказаться сложной и отнимающей много времени задачей. Одной из таких оптимизаций в Next.js, например, является разделение кода на основе маршрутов маршрутизатора страниц, который использовался по умолча­ нию для Next.js 13 и более ранних версий. Каждая страница в этом случае авто­ матически разбивается кодом на части и образует свой пакет, который затем за­ гружается только при запросе страницы. производительность ния за счет уменьшения Это может значительно повысить начального размера пакета и улучше­ TTFB. Опыт разработчика и продуктивность. Создание пользовательской реализации серверного рендеринга может быть сложной и отнимающей много времени задачей. Используя фреймворк, подоб­ ный Next.js или Remix, разработчики могут сосредоточиться на создании функций для своего приложения, вместо того чтобы беспокоиться о базовой инфраструк­ туре серверного рендеринга. Это может привести к повышению производитель­ ности и улучшению общего опыта разработчиков. Лучшие практики и соглашения. Использование таких фреймворков, как Next.js или Remix, может помочь вне­ дрить лучшие практики и соглашения в ваш проект. Эти фреймворки были раз­ работаны с учетом лучшего опыта, и, если следовать их соглашениям, можно гарантировать, что ваше приложение будет построено на прочном фундаменте: // Пример лучшего пракмического решения с помощью Reмix. // Файл: гoutes/posts/$postid. tsx 'U'1port { usePara111s} fгО/11 "react-router-do111"; 'U'1port { useloaderData} fгО/11 "@re111i.x-run/react"; // // - раннее извлечение данных. - совмесмное размещение данных ехрогt function loader({ para111s }) { гetuгn fetchPost(para111s.postld); Лучшая пракмика Лучшая пракмика } functton Post() { const { postld} = usePara111s(); const post = useLoaderData(); гetuгn ( <dtv> <h1>{post.ti.tle}</h1> с UI
220 Глава 6 <div>{post.content}</div> </div> ); } ехрогt default Post; С учетом преимуществ и оптимизации, предоставляемых известными фреймворка­ ми, такими как Next.js и Remix, становится очевидным, что создание пользователь­ ского решения для серверного рендеринга приложений не является идеаль­ React ным подходом. С помощью этих платформ вы можете с-экономить массу времени на разработку, обеспечить использование передового опыта и воспользоваться по­ стоянными улучшениями и поддержкой, предоставляемыми соответствующими сообществами. Обзор главы В заключение отметим, что серверный рендеринr и гидратация - это мощные мето­ ды, которые могут значительно повысить производительность, удобство для пользо­ вателей и SЕО-оптимизацию веб-приложений. API React предоставляет богатый набор для серверного рендеринга, таких как renderToStri.ng и renderToPi.peaЫeStreal'1, каждый из которых имеет свои сильные стороны и недостатки. Разобравшись в этих АРI-интерфейсах и выбрав правильный из них с учетом таких факторов, как размер приложения, серверная среда и опыт разработчика, вы сможете оптимизи­ ровать свое приложение React с целью повышения производительности как на сто­ роне сервера, так и на стороне клиента. Как мы уже видели в этой главе, renderToStri.ng - это простой и понятный API для серверного рендеринга, который подходит для небольших приложений. Однако он может оказаться не самым эффективным вариантом для больших приложений из-за его синхронного характера и возможности блокировать цикл обработки событий. С другой стороны, renderToPi.peaЫeStreaf'l - это более продвинутый и гибкий API, который позволяет лучше контролировать процесс рендеринга и улучшает инте­ грацию с другими потоками Node.js, что делает его более подходящим выбором для более крупных приложений. Проверьте ваши знания Теперь, когда вы получили четкое представление о серверном рендеринге и гидра­ тации в React, пришло время проверить ваши знания, задав вам несколько обзор­ ных вопросов. Если вы сможете уверенно ответить на них, это хороший признак того, что вы уже неплохо разбираетесь в механизмах React и можете спокойно про-
Серверный React 1 221 двигаться вперед. Если у вас не получается, хотя это не повредит общему воспри­ ятию материала книги, прислушайтесь к совету ознакомиться с материалами чуть подробнее. 1. В чем основное преимущество использования рендеринга на стороне сервера в приложении React? 2. Как работает гидратация в 3. Что такое возобновляемость? В чем ее преимущество перед гидратацией? 4. React и почему важно понимать механизм ее работы? Каковы основные преимущества и недостатки рендеринга только на стороне клиента? 5. В чем основные различия между АРI-интерфейсами renderT0Pi.peaЫeStrea111 в renderT0ReadaЫeStrea111 и React? Что дальше? Как только вы освоите серверный рендеринг и гидратацию, вы будете готовы изу­ чить более сложные темы разработки конкурентный React. React. В следующей главе мы рассмотрим Поскольку веб-приложения усложняются, обработка асин­ хронных действий становится все более важной для обеспечения удобства работы пользователей. Научившись использовать конкурентный React, вы сможете создавать высокопро­ изводительные, масштабируемые и удобные в использовании приложения, которые с легкостью справляются со сложными взаимодействиями с данными. Так что го­ товьтесь продолжить совершенствование своих навыков в лекательное путешествие в мир конкурентного React! React и начать наше ув­

ГЛАВА Конкурентный 7 React В предыдущей главе мы углубились в мир серверного рендеринга, который построи­ ли с помощью React. Мы изучили важность рендеринга на стороне сервера для по­ вышения производительности и удобства работы с нашими приложениями, осо­ бенно в контексте современной веб-разработки. Мы изучили различные АРI-интер­ фейсы серверного рендеринга, такие как renderToStri.ng и renderToPi.peaЫeStreal'1, и обсудили их преимущества и варианты использования. Мы также затронули про­ блемы реализации рендеринга на стороне сервера и показали, что лучше полагаться на устоявшиеся фреймворки, такие как Next.js и Remix, которые помогут нам спра­ виться со сложностями. Мы рассмотрели концепцию гидратации и ее значение для объединения серверной разметки с клиентскими компонентами React, что важно для обеспечения комфорт­ ной работы пользователей. Кроме того, мы обсудили потенциальные проблемы безопасности и сложности, связанные с управлением несколькими клиентскими подключениями в серверной среде, подчеркнув необходимость использования платформ, которые эффективно решают эти проблемы. Теперь, когда мы переходим к следующей теме - конкурентном/ React, мы будем опираться на наше понимание всего, что мы узнали на данный момент. Мы позна­ комимся с FiЬеr-согласователем и узнаем об особенностях конкурентного также о том, насколько эффективно React React, а управляет обновлениями и рендерингом. Изучая планирование, отсрочку обновлений и полосы рендеринга, мы получим представление об оптимизации производительности, которая стала возможной бла­ годаря базовой архитектуре ~ React. Еще раз стоит отметить, что сама FiЬеr-архитектура и аспекты, которые мы собираемся обсудить, - это детали реализации в React, которые, скорее всего, изменятся и которые вам не обязательно знать, чтобы эффективно использовать React. Однако изучение базовых механизмов поможет вам лучше понять, как работает React и как его эффективно применять, а также повысит ваши знания как инженера в целом. 1 Конкурентный (concurrent) рендеринг - способность React сделать процесс рендеринга прерывае­ мым для выполнения более приоритетных задач, таких как, например, обработка ввода данных поль­ зователем. - Прим. пер.
224 Глава 7 Давайте теперь отправимся в наше путешествие в увлекательный мир конкурентно­ го React. Мы продолжаем развивать наш опыт и открывать новые способы исполь­ зования возможностей React для создания высокопроизводительных приложений. Проблема синхронного рендеринга Напомним, что проблема синхронного рендеринга заключается в том, что он бло­ кирует основной поток, а это может привести к ухудшению взаимодействия с поль­ зователем. Такая блокировка особенно актуальна для сложных приложений с большим количеством компонентов и частыми обновлениями. В таких случаях пользовательский интерфейс может перестать отвечать на запросы, что приводит к разочарованию пользователей. Типичный способ решения этой проблемы - объединить серию обновлений в одно обновление и свести к минимуму работу, выполняемую в главном потоке: вместо того, чтобы обработать 1О задач 1О раз, объедините задачи в пакет и обработайте этот пакет единожды. Мы рассматривали пакетную обработку в главе 4, поэтому здесь не будем вдаваться в подробности, но для целей нашего обсуждения важно понимать, что пакетная обработка - шаг по устранению этих проблем, но и здесь есть ограничения, о чем мы расскажем в следующих нескольких разделах. Проблемы, о которых мы говорили, даже при использовании пакетной обработки, еще больше усугубляются тем фактом, что по своей природе синхронный ренде­ ринг не имеет приоритета. Обновления обрабатываются независимо от их видимо­ сти. При синхронном рендеринге вы можете заблокировать основной поток, вы­ полняя рендеринг элементов, которые пользователь не может видеть в данный момент, например невидимые вкладки, содержимое, скрытое за модальным окном, или содержимое, находящееся в состоянии загрузки. Вы по-прежнему желаете, чтобы эти элементы отображались при наличии доступного ресурса процессора, но вы хотите расставить приоритеты рендеринга того, что пользователь может видеть и с чем он может взаимодействовать. До того как в React появились конкурентные возможности, у нас часто возникали ситуации, когда критические обновления бло­ кировались менее важными, что приводило к ухудшению взаимодействия с пользо­ вателем. Благодаря конкурентному рендерингу React может расставлять приоритеты обнов­ лений в зависимости от их важности и срочности, гарантируя, что критические об­ новления не будут заблокированы менее важными. Это позволяет React поддержи­ вать адаптивный пользовательский интерфейс даже при большой нагрузке, что улучшает взаимодействие с пользователем. Например, когда пользователь наводит курсор мыши на кнопку или нажимает на нее, ожидается, что он немедленно полу­ чит обратную связь в соответствии с этим действием. Если React работает над по­ вторным отображением длинного списка элементов, то обратная связь при наведе­ нии курсора мыши или новом состоянии будет отложена до тех пор, пока не будет отображен весь список. Благодаря конкурентному рендерингу задачи визуализации, которые требуют больших затрат ресурсов центрального процессора, могут быть
Конкурентный React 1 225 отодвинуты на второй план по сравнению с более важными задачами рендеринга, такими как взаимодействие с пользователем и анимация. Более того, благодаря возможностям конкурентного рендеринга React способен де­ лать "временную нарезку", которая заключается в том, что он может разбить про­ цесс рендеринга на более мелкие фрагменты и обрабатывать их постепенно. Это позволяет React выполнять работу с несколькими кадрами, и если работу нужно прервать, это можно сделать. Давайте рассмотрим все это более подробно. Возвращаясь к Fiber Как говорилось в главе 4, FiЬеr-согласователь (Fiber reconciler) является основным механизмом в React, ден в и представлял собой значительный архитектурный сдвиг по сравне­ React 16 который обеспечивает конкурентный рендеринг. Он был вве­ нию с предыдущим стек-согласователем согласователя React, - (stack reconciler). Основная цель FiЬеr­ повысить скорость отклика и производительность приложений особенно для больших и сложных пользовательских интерфейсов. FiЬеr-согласователь достигает этого, разбивая процесс рендеринга на более мелкие, более управляемые части, называемые Fibers ("волокна"). В результате становятся возможными остановка, возобновление и определение приоритетности задач рен­ деринга, которые позволяют отложить или запланировать обновления в зависимости от их важности. Это повышает скорость реагирования приложения и гарантирует, что критические обновления не будут заблокированы менее важными задачами. Планирование и отсрочка обновлений Для поддержки адаптивного приложения способность React планировать и откла­ дывать обновления имеет решающее значение. FiЬеr-согласователь обеспечивает эту функциональность, опираясь на планировщик и ряд эффективных позволяют React API. Эти API организовать работу в периоды простоя и планировать обновления в наиболее подходящее время. Мы проанализируем реализацию планировщика подробнее в следующих разделах, а пока рассмотрим его с точки зрения названия. Планировщик - это система, кото­ рая получает обновления и сообщает: "вы делаете это сейчас", "вы делаете это поз­ же" и т. д., используя АРI-интерфейсы браузера, такие как setП.l'leout, МessageChannel и им подобные. Рассмотрим приложение для общения в режиме реального времени (чат), в котором пользователи могут отправлять и получать сообщения. Мы имеем компонент чата, отображающий список сообщений, и компонент ввода сообщений, где пользовате­ ли могут печатать и отправлять свои сообщения. Кроме того, чат получает новые сообщения с сервера в режиме реального времени. В этом сценарии мы хотим рас-
Глава 226 7 ставить приоритеты для взаимодействия с пользователем (ввод текста и отправка сообщений), чтобы организовать оперативное взаимодействие и при этом обеспе­ чить эффективное отображение входящих сообщений без блокировки пользова­ тельского интерфейса ввода. Для того чтобы сделать этот пример более конкретным, давайте создадим несколь­ ко компонентов. Сначала список сообщений: const Messageltst = ({ ~essages }) => ( <Ul> {мessages.мap((l'lessage, tndex) => ( <lt key={tndex}>{мessage}</lt> ))} </ul> Далее, у нас есть компонент ввода сообщений, который позволяет печатать и от­ правлять сообщения: const Messageinput = ({ onSubмtt }) => { const [мessage, setMessage] = useState(""); const handleSubмtt = (е) => { e.preventDefault(); onSubмtt(мessage); setMessage(""); }; гetuгn ( <fогм onSubмtt={handleSubмtt}> <tnput type="text" value={мessage} onChange={(e) => setMessage(e.target.value)} /> <button type="subмH">Send</button> </fогм> ); }; Наконец, у нас есть компонент чата, который объединяет эти два компонента и управляет логикой отправки и получения сообщений: const ChatApp = () => { const [l'lessages, setMessages] = useState([]);
Конкурентный React 1 227 useEffect(() => { // Подключаемся к серверу и подписываемся на входящие сообщения const socket = new WebSocket("wss://youг-websocket-server.cOl'I"); socket.on111essage = (event) => { setМessages((prevMessages) => [ ... prevМessages, event.data]); }; retuгn () => { socket. с lose(); }; }, []); const sendМessage = (111essage) => { // Оl'mравляем сообщение серверу }; гetuгn ( <d'iv> <МessageLtst 111essages={111essages} /> <Мessageinput onSuЬl'lit={sendМessage} /> </d'iv> ); }; В этом примере возможности React по конкурентному отображению вступают в силу благодаря эффективному управлению как обновлениями списка сообщений, так и обновлениями действий пользователя с вводимыми сообщениями. Когда пользователь вводит или отправляет сообщение, React устанавливает приоритет обновлений для ввода текста над другими обновлениями, чтобы обеспечить беспе­ ребойную работу пользователя. Когда с сервера поступают новые сообщения, которые необходимо вывести на экран, они отображаются в полосе рендеринга по умолчанию, который обновляет DOM асинхронно и мгновенно блокирующим образом, что приведет к задержке любого пользовательского ввода. Если мы хотим, чтобы приоритет отображения нового списка сообщений был снижен, мы можем обернуть соответствующее обновление состояния в функцию startTransHton хука useTransHton, const ChatApp = () => { const [111essages, setМessages] = useState([]); const [tsPendtng, startTransttton] = useTransttton(); useEffect(() => { // Подключаемся следующим образом: к серверу и подписываемся на входящие сообщения
228 Глава 7 const socket = new WebSocket("wss://your-websocket-server.c0/11 11 ); socket.onl'lessage = (event) => { startTransttton(() => { setМessages((prevМessages) => [ ... prevМessages, event.data]); }); }; () => { socket.close(); гetuгn }; }, []); const sendМessage = (l'lessage) => { // Оl'lправляем сообщение серверу }; ( гetuгn <di.v> <МessageLi.st l'lessages={l'lessages} /> <Мessageinput onSuЬritt={sendМessage} /> </di.V> ); }; Таким образом, мы даем сигнал React запланировать обновления списка сообщений с более низким приоритетом и отображать их без блокировки пользовательского интерфейса, позволяя приложению чата эффективно функционировать даже при большой нагрузке. В этом случае ввод данных пользователем никогда не прерыва­ ется, а входящие сообщения отображаются с меньшим приоритетом, поскольку они менее критичны для взаимодействия с пользователем. Этот пример демонстрирует, как можно использовать конкурентный рендеринг React для создания адаптивных приложений, которые справляются со сложными взаимодействиями и частыми обновлениями без ущерба для производительности или удобства пользователей. В этой главе мы детально рассмотрим пока давайте подробнее разберем, как именно React useTransttton. А планирует обновления. Погружаемся глубже В React процесс планирования, определения приоритетов и отсрочки обновлений важен для поддержания адаптивного пользовательского интерфейса. Этот процесс гарантирует оперативное решение высокоприоритетных задач, в то время как зада-
Конкурентный React 229 чи с низким приоритетом могут быть отложены, что позволяет пользовательскому интерфейсу оставаться отзывчивым даже при большой нагрузке. Для того чтобы углубиться в эту тему, мы рассмотрим несколько основных концепций: планиров­ щик, уровни приоритета задач и механизмы, которые откладывают обновления. Прежде чем мы продолжим, давайте еще раз напомним себе, что информа­ ция, представленная здесь, состоит из деталей реализации и не является обязательной для того, чтобы научиться использовать React. Однако по­ нимание этих концепций поможет вам лучше понять, как работает React и как эффективно его использовать, а также научит вас базовому механизму, который вы сможете применять в других инженерных областях, улучшая тем самым свой общий набор навыков. Имея это в виду, давайте продолжим. Планировщик С точки зрения архитектуры React, планировщик - это автономный пакет, кото­ рый предоставляет утилиты, связанные с синхронизацией. React использует плани­ ровщик в составе FiЬеr-соrласователя. Планировщик и согласователь, используя полосы рендеринга, позволяют задачам взаимодействовать с учетом приоритетов и упорядочивают их в зависимости от их срочности. Вскоре мы подробно рассмот­ рим полосы рендеринга. Основная роль планировщика в React сегодня - управ­ лять производительностью основного потока, главным образом путем планирова­ ния микрозадач для обеспечения бесперебойного и гладкого выполнения. Для того чтобы понять это в деталях, давайте рассмотрим часть исходного кода React ехрогt (на момент написания книги): functi.on ensureRootlsScheduled(root: FiberRoot): void { // ЭРlа функция вызываемся, когда гооt получаем обновление. Она делаем две вещи: // 1) гаранмируем, чмо гооt находимся в корневом расписании, и // 2) гаранмируем, чмо сущесмвуем незавеf)lllенная микрозадача // для обрабомки корневого расписания. // // Большая часмь факмической логики планирования не выполняемся до мех пор, // пока не будем запущена scheduleTaskForRootDuгingMicгotask. // Добавим гооt в расписание tf (гооt === lastScheduledRoot 11 root.next !== null) { // Бысмрый пумь. ЭРlом гооt уже запланирован. } else { tf (lastScheduledRoot === null) { firstScheduledRoot = lastScheduledRoot } else { lastScheduledRoot.next = гооt; = гооt;
230 Глава 7 lastScheduledRoot = root; } } // // // // Каждый раз, когда гооt получаеf'I обновление, ,,.,, усf'lанавливаем для эf'lого napaмef'lpa значение tгие до следукицей обрабоf'lки расписания. Если значение равно false, ,,.,, можем бысf'lро завершиf'lь flushSync, не заглядывая в расписание. мightHavePendingSyncWork = tгue; // В конце f'lекущего собыf'luя npocмof'lpuf'le каждый гооt и убедuf'lесь, // Чf'IO для них заманирована задача с правильным npuopиf'lef'loм. tf (_DEV_ && ReactCurrentActQueue.current !== null) { // Мы в oблacf'lu видимосf'lи act. tf (!didScheduleMicrotask_act) { didScheduleMicrotask_act = tгue; schedulelммediateTask(processRootSchedulelnMicrotask); } } else { tf (!didScheduleMicrotask) { didScheduleMicrotask = tгue; schedulelммediateTask(processRootSchedulelnMicrotask); } } tf (!enaЫeDeferRootSchedulingToMicrotask) { // Пока Эf'IOf'I флаг Оf'lключен, ,,.,, манируем начаf'lь рендеринг // вмесf'lо f'loгo, Чf'lобы ждаf'lь выполнения микрозадачи. scheduleTaskForRootDuringMicrotask(root, now()); немедленно } tf ( _DEV_ && ReactCurrentActQueue.isBatchinglegacy && root.tag === LegacyRoot ) { // Особый случай "act": записывайf'lе каждый раз, когда // усf'lаревшее обновление. ReactCurrentActQueue.didSchedulelegacyUpdate = tгue; } } запланировано
Конкурентный Функция ensureRootischeduled в кодовой базе React React 1 231 играет решающую роль в управ­ лении процессом рендеринга. Когда корневой узел DOM гооt, представленный гооt: Fi.ber Root, получает обновление, эта функция вызывается для выполнения двух важных действий. Помните из главы 4: React гооt- это финальный "своп 2 ", который происходит на этапе фиксации внесения обновлений. Когда вызывается функция ensureRootisScheduled, во-первых, это подтверждает, что корневой узел гооt включен в корневое расписание - список, который отслежива­ ет, какие корневые узлы должны быть обработаны. Во-вторых, это гарантирует на­ личие незавершенной микрозадачи, предназначенной для обработки этого корнево­ го расписания. Микрозадача- это концепция управления циклом событий JavaScript, представ­ ляющая тип задачи, которая управляется очередью микрозадач. Для того чтобы по­ нять концепцию микрозадач, важно сначала получить общее представление о цикле событий JavaScript и связанных с ним очередях задач. Цикл обработки событий. Движок JavaScript использует цикл обработки событий для управления асин­ хронными операциями. Цикл обработки событий постоянно проверяет, есть ли какие-либо работы (например, выполнение обратного вызова), которые должны быть сделаны. Он работает с двумя типами очередей задач: очередь задач (оче­ редь макрозадач) и очередь микрозадач. Очередь задач (очередь макрозадач). В этой очереди хранятся такие задачи, как обработка событий, выполнение об­ ратных вызовов setТi.мeout и setinterva l, а также выполнение операций ввода­ вывода. Задачи в этой очереди обрабатываются последовательно, и следующая задача выполняется только после завершения текущей. Очередь микрозадач. Микрозадача - это мелкая, но более срочная задача. Микрозадачи возникают в результате таких операций, как обещания (промисы Mutati.onObserver. - promise), Object.observe и Они хранятся в очереди микрозадач, которая отличается от обычной очереди задач. Исполнение. Микрозадачи обрабатываются в конце текущей задачи, прежде чем движок JavaScript выберет следующую (макро) задачу из очереди задач. После выпол­ нения задачи модуль проверяет, есть ли какие-либо микрозадачи в очереди мик­ розадач, и выполняет их все, прежде чем двигаться дальше. Это гарантирует, что микрозадачи будут обработаны быстро и по порядку, сразу после текущего ис­ полняемого скрипта и перед любыми другими задачами, такими как рендеринг или обработка событий. 2 Своп (swap)- (в финансах) обменная операция в виде обмена разнообразными активами. - пер. Прим.
232 Глава 7 Характеристики и использование. Микрозадачи имеют более высокий приоритет по сравнению с другими задача­ ми в очереди задач, т. е. они выполняются перед переходом к следующей макро­ задаче. Если микрозадача постоянно добавляет новые микрозадачи в очередь, это может привести к тому, что очередь задач никогда не будет обработана. Этот процесс называется "голоданием" В контексте React (starvation). и функции ensuгeRootisScheduled микрозадача используется для обеспечения того, чтобы обработка корневого расписания происходила быстро и с высоким приоритетом, сразу после выполнения текущего скрипта и до того, как браузер выполнит другие задачи, такие как рендеринг или обработка событий. Это помогает поддерживать плавное обновление пользовательского интерфейса и эф­ фективное управление задачами в рамках фреймворка Функция запускается с добавления корневого узла ет в себя проверку того, является ли root React. (root) в расписание. Это включа­ последним запланированным или уже присутствует в расписании. Если он отсутствует, функция добавляет расписания, обновляя ранее ни один root lastScheduledRoot, root в конец root. Если null), текущий root чтобы он указывал на текущий не был запланирован (lastScheduledRoot === становится первым и последним в расписании. Далее функция устанавливает значение true для флага мi.ghtHavePendi.ngSyncWork. Этот флаг сигнализирует о том, что, возможно, требуется выполнить синхронную рабо­ ту, необходимую для функции flushSync, о которой мы расскажем в следующем разделе. Затем функция убеждается, что микрозадача запланирована для обработки корне­ вого расписания. Это делается schedu leiммedi.ateTask( processRootSchedu leinMi.crotask). с помощью Планирование вызова выполняется как внутри, так и за пределами области действия утилиты тестирования обозначенной React act, _DEV_andReactCurrentActQueue. current. Другой важной частью этой функции является блок условий, проверяющий флаг enaЫeDeferRootScheduli.ngToMi.crotask. Если этот флаг отключен, функция планирует выполнение задачи рендеринга немедленно, вместо того чтобы откладывать ее до выполнения микрозадачи. Эта часть помечена комментарием тооо (на момент напи­ сания книги), указывающим на будущие планы по включению этой функции для разблокировки дополнительной функциональности. Наконец, функция включает в себя условие для обработки устаревших 3 обновлений в утилите act. Это относится к сценариям тестирования, в которых обновления рас­ пределяются по-разному, и функция отмечает, когда запланировано устаревшее обновление. Суть в том, что ensureRootisScheduled- это сложная функция, которая объединяет несколько аспектов логики планирования и рендеринга 3 React, фокусируясь на эф- Устаревшее обновление (код; legacy- наследство, наследие)- старый, часто работающий уже неэффективно, код, который нужно поддерживать наравне с новым. - Прим. пер.
Конкурентный фективном управлении обновлениями для React roots React 233 и обеспечении плавного рен­ деринга за счет стратегического планирования задач и микрозадач. Исходя из этого, мы понимаем роль планировщика в React: планирование работы основано на полосах рендеринга, на которые попадает работа по визуализации. В следующем разделе мы подробно рассмотрим полосы, а пока достаточно сказать, что полосы указывают на приоритет обновления. Если мы смоделируем поведение планировщика в коде, это будет выглядеть сле­ дующим образом: tf (nextlane === Sync) { queueМicrotask(processNextlane); } else { Scheduler.scheduleCallЬack(callback, processNextlane); } Из этого мы видим, что: ♦ если следующая полоса - Sync, то планировщик ставит микрозадачу в очередь для немедленной обработки этой полосы. В идеале, к настоящему моменту мы уже понимаем, что такое микрозадачи и как это работает; ♦ если следующая полоса не Sync, планировщик планирует обратный вызов и об- рабатывает следующую полосу. Таким образом, планировщик - это именно то, чем он должен быть: система, которая планирует выполнение функций в зависимости от полосы рендеринга. Итак, мы уже начали обсуждение полос. Давайте углубимся и разберемся в них подробнее! Полосы рендеринга Полосы рендеринга являются важной частью системы планирования React, которая обеспечивает эффективную визуализацию и приоритезацию задач. Полоса - это единица работы, которая представляет уровень приоритета и может обрабатываться React в рамках цикла рендеринга. Концепция полос рендеринга была введена в React 18, заменив предыдущий механизм планирования, в котором использовалось время истечения срока действия. Давайте рассмотрим детали полос рендеринга, принцип их работы и их базовое представление в виде битовых масок. ~ Еще раз повторю, что это детали реализации в React, которые могут изме­ ниться в любой момент. Суть здесь в том, чтобы понять базовый механизм, который поможет нам в нашей повседневной инженерной работе, а также даст возможность понять, как работает React, и позволит нам использовать его эффективнее и свободнее. Нам было бы полезно не зацикливаться на деталях, а вместо этого сосредоточиться на базовом механизме потенциале для применения в реальном мире. React и его
234 Глава 7 Во-первых, полоса рендеринга - это упрощенная абстракция, используемая React для организации и определения приоритетов обновлений, которые необходимо вы­ полнить в процессе визуализации. Например, когда вы вызываете setState, это обновление помещается в полосу. Мы можем понять различные приоритеты, основываясь на контексте обновлений, на­ пример: ♦ если setState вызывается внутри обработчика кликов, он помещается в полосу синхронизации Sync (с наивысшим приоритетом) и планируется как микро­ задача; ♦ если setState вызывается внутри перехода из startTransi.ti.on, он помещается в полосу перехода (с более низким приоритетом) и планируется как микрозадача. Каждая полоса соответствует определенному уровню приоритета, при этом полосы с более высоким приоритетом обрабатываются раньше полос с более низким при­ оритетом. Вот несколько примеров полос в React. SyncHydrati.onlane Когда пользователи щелкают в приложении cli.ck переносится Synclane React во время гидратации, событие в эту полосу. Когда пользователи щелкают в приложении React, событие cli.ck переносится в эту полосу. InputConti.nuousHydrati.onlane События наведения курсора мыши, прокрутки и другие непрерывные события во время гидратации помещаются в эту полосу. InputConti.nuousLane Аналогично предыдущему, но после гидратации приложения React. Defaultlane Все обновления из сети, таймеры, такие как setТi.мeout, и начальный рендеринг, приоритет которого не определен, переносятся в эту полосу. Transi.ti.onHydrati.onlane Все переходы от startTransi.ti.on во время гидратации переносятся в эту полосу. Transi.ti.onlanes (1-15) Все переходы от начального перехода после гидратации заносятся в эти полосы. Retrylanes ( 1--4) Любые повторные попытки в режиме Suspense переносятся на эти полосы. Стоит отметить, что эти полосы описывают внутреннюю реализацию React мент написания книги и могут быть изменены. Повторяю, цель этой книги нять механизм, с помощью которого работает React, на мо­ - по­ не вдаваясь в подробности реализации, поэтому названия полос, скорее всего, не имеют большого значения. Гораздо важнее наше понимание механизма, т. е. того, как React концепцию полос, и то, как мы можем применить ее в нашей работе. использует эту
Конкурентный React 235 Как работают полосы рендеринга Когда компонент обновляется или новый компонент добавляется в дерево ренде­ ринга, React назначает обновлению полосу на основе его приоритета, используя типы полос, упомянутые ранее. Как мы знаем, приоритет определяется видом об­ новления (например, взаимодействие с пользователем, выборка данных или фоно­ вая задача) и другими факторами, такими как видимость компонента. Затем React использует полосы рендеринга для планирования обновлений и опре­ деления их приоритетов следующим образом. 1. Сбор обновлений. React собирает все обновления, которые бьши запланированы с момента последнего рендеринга, и назначает их соответствующим полосам согласно их приоритету. 2. Обработка полос. React обрабатывает обновления в соответствующих полосах, начиная с полосы с наивысшим приоритетом. Обновления каждой полосы собираются в пакет и об­ рабатываются за один проход. 3. Этап фиксации. После обработки всех обновлений он применяет изменения к по финализации 4. React переходит к этапу фиксации, где DOM, запускает эффекты и выполняет другие задачи изменений DOM. Повторение. Процесс повторяется для каждого рендеринга, гарантируя, что обновления все­ гда обрабатываются в порядке их приоритетов и высокоприоритетные обновле­ ния не будут вытеснены более низкоприоритетными. React заботится о назначении правильных полос на основе приоритетов, что позво­ ляет приложению эффективно обновляться без ручного вмешательства. Когда запускается обновление, React, чтобы определить его приоритет и присвоить ему правильную полосу, выполняет следующие действия: 1. Определение контекста обнов.1ения. React оценивает контекст, в котором было запущено обновление. Этим контек­ стом может быть взаимодействие с пользователем, внутреннее обновление, вы­ званное изменениями состояния или пропсов, или даже обновление, являющееся результатом ответа сервера. Контекст играет решающую роль в определении приоритета обновления. 2. Оценка приоритета на основе контекста. На основе контекста React оценивает приоритет обновления. Например, если обновление является результатом ввода данных пользователем, оно, скорее все­ го, будет иметь более высокий приоритет, в то время как обновление, запущен­ ное некритичным фоновым процессом, будет иметь более низкий приоритет. Мы уже подробно обсуждали различные уровни приоритета, поэтому не будем вдаваться в подробности.
236 3. Глава 7 Проверка переопределений приоритетов. В некоторых случаях разработчики могут явно устанавливать приоритет обнов­ ления, используя хуки определение React useTransi.ti.on или useDeferredVa lue. Если такое пере­ приоритета имеет место, React будет учитывать предоставленный приоритет вместо предполагаемого. 4. Назначение обновлению правильной полосы. Как только приоритет определен, React назначает обновлению соответствую­ щую полосу. Этот процесс выполняется с использованием битовой маски, кото­ рую мы только что рассмотрели, что позволяет React эффективно работать с не­ сколькими полосами и обеспечивать правильную группировку и обработку обновлений. На протяжении всего этого процесса для принятия обоснованных решений о при­ оритетах React полагается на свои внутренние эвристики и контекст, в котором происходят обновления. Такое динамическое присвоение приоритетов и полос рен­ деринга позволяет React сбалансировать скорость отклика и производительность, обеспечивая эффективную работу приложений без ручного вмешательства разра­ ботчиков. Давайте разберемся, как именно React обрабатывает обновления на назначенных им полосах. Обработка полос После того как обновления получили соответствующие полосы, React обрабатывает React обрабаты­ их в порядке приоритетов. В нашем примере приложения для чата вает обновления в следующем порядке. Il'l'ledi.atePri.ori.ty Обновления при вводе сообщений, гарантирующие, что приложение будет реа­ гировать и быстро обновляться. UserBlocki.ngPri.ori.ty Обновления индикатора набора текста, предоставляющие пользователям обрат­ ную связь в режиме реального времени. Norl'\alPri.ori.ty Обновления списка сообщений, отображающие новые сообщения и обновления с разумной скоростью. Обрабатывая обновления в приоритетном порядке, React гарантирует, что наиболее важные части приложения остаются отзывчивыми даже при большой нагрузке. Фаза фиксации После обработки всех обновлений в соответствующих полосах фазе фиксации, где он применяет изменения к DOM, React переходит к запускает эффекты и выпол-
Конкурентный React 237 няет другие финализирующие задачи. В нашем примере приложения для чата это может включать обновление ввода сообщения, отображение или скрытие индика­ тора ввода и добавление новых сообщений в список сообщений. Затем React пере­ ходит к следующему циклу рендеринга, повторяя процесс сбора обновлений, обра­ ботки полос и фиксации изменений. Однако этот процесс значительно сложнее, чем мы можем по-настоящему оценить в этой книге: существуют такие концепции, как "запутанность" (entanglement), которая определяет, когда две полосы должны обрабатываться совместно. Другая концепция, например "перебазирование" (rebasing), определяет, когда обновление должно быть перебазировано поверх обновлений, которые уже были обработаны. Перебазирование полезно, например, в тех случаях, когда переход (transition) пре­ рывается до его завершения из-за обновления синхронизации, и вам нужно запус­ тить вместе оба обновления. Здесь также можно многое сказать об эффектах сброса мер, при синхронном обновлении чтобы обновления, React (flushing effects). Напри­ может сбрасывать эффекты до или после обеспечить согласованное состояние синхронизированных обновлений. В конечном счете именно для этого существует React, и истинная ценность React заключается в том, что он работает за кулисами как уровень абстракции: он в ос­ новном решает проблемы с обновлениями, их приоритезацией и упорядочиванием за нас, в то время как мы продолжаем фокусироваться на наших приложениях. Важно отметить, что, хотя React хорошо оценивает приоритеты, он не всегда идеа­ лен. Как разработчику, вам иногда может потребоваться изменить приоритет зада­ ния по умолчанию, используя некоторые useTransi.ti.on и useDeferredValue - API, о которых мы уже упоминали - для точной настройки производительности и .от­ зывчивости вашего приложения. Давайте рассмотрим эти API более подробно. use Transition useTransi.ti.on - это мощный хук React, который позволяет вам управлять приори­ тетом обновлений состояния ваших компонентов и гарантировать, что пользова­ тельский интерфейс останется отзывчивым в случае высокоприоритетных обновле­ ний. Это особенно полезно, когда приходится иметь дело с обновлениями, которые могут нарушить отрисовку, например, при загрузке новых данных или навигации между страницами. По сути, любое обновление, которое вы оборачиваете в возвращаемую функцию startTransi.ti.on, помещается в полосу перехода (transition lane), которая, как мы ви­ дели, имеет более низкий приоритет, чем полоса синхронизации (Sync lane). Это позволяет контролировать время обновления и поддерживать бесперебойную рабо-
Глава 238 7 ту пользователя, даже когда другие обновления с более высоким приоритетом кон­ курируют за доступ к основному потоку. useTransi.tt.on - это хук, который можно использовать только внутри функциональ­ ных компонентов. Он возвращает массив, содержащий два элемента. i.sPendi.ng Логическое значение, указывающее, выполняется ли переход. Одна интересная деталь работы useTransi.ti.on заключается в том, что первое, что он делает, когда startTransi.ti.on, - это установка setState( { i.sPendi.ng: false }), оз­ начающая, что обновления, зависящие от i.sPendi.ng, должны быть быстрыми, иначе это противоречит цели useTransi.ti.on. startTransi.ti.on вы вызываете Функция, которую можно использовать для оборачивания обновлений, которые следует отложить или которым следует присвоить более низкий приоритет. Вероятно, здесь стоит упомянуть, что существует также API startTransi.ti.on, кото­ рый доступен не как хук, а как обычная функция. Второй способ запустить несроч­ ный переход - средственно из использовать функцию React. startTransi.ti.on, импортированную непо­ Этот подход не дает нам доступа к флагу i.sPendi.ng, но он доступен для тех мест в вашем коде, где вы не можете использовать хуки, напри­ мер useTransi.ti.on, но все равно хотите сигнализировать React об обновлении с низ­ ким приоритетом. Простой пример Вот простой пример, демонстрирующий useTransi.ti.on: viport React, { useState, useTransi.ti.on} frOl'I основные "геасt"; functt.on Арр() { const [count, setCount] = useState(0); const [i.sPendi.ng, startTransi.ti.on] = useTransi.ti.on(); const handleCli.ck = () => { doS0111ethi.ngI~portant(); startTransi.ti.on(() => { setCount(count + 1); }); }; return ( <di.v> <p>Count: {count}</p> принципы использования
Конкурентный React 1 239 <button onCli.ck={handleCli.ck}>Incre~ent</button> {i.sPendi.ng && <p>Loadi.ng ... </p>} </di.v> ); } ехрогt default Арр; В этом примере мы используем useTransi.tton для управления приоритетом обнов­ ления состояния, которое увеличивает счетчик. Помещая обновление функцию startTransi.ti.on, мы сообщаем React, setCount в что это обновление может быть от­ ложено, если одновременно происходят другие высокоприоритетные обновления. Цель в том, чтобы пользовательский интерфейс не перестал отвечать на запросы. Более сложный пример: навигация useTransi.ti.on также полезен при организации переходов между страницами. Управ­ ляя приоритетом обновлений, связанных с навигацией, вы можете обеспечить плавность и отзывчивость при взаимодействии с пользователем даже в случае сложных переходов по страницам. Рассмотрим пример, в котором мы продемонстрируем, useTransi.ti.on для управления переходами в одностраничном page application, SPA): 'U'lport React, { useState, useTransi.ti.on} frOl'I "react"; const PageOne = () => <di.v>Page One</di.v>; const PageTwo = () => <di.v>Page Two</di.v>; functi.on Арр() { const [currentPage, setCurrentPage] = useState("pageOne"); const [i.sPendi.ng, startTransi.ti.on] = useTransi.ti.on(); const handleNavi.gati.on = (page) => { startTransi.ti.on(() => { setCurrentPage(page); }); }; const renderPage = () => { swi.tch (currentPage) { case "pageOne": гetuгn <PageOne />; как использовать приложении (single-
240 Глава 7 case "pageTwo": return <PageTwo />; default: return <div>Unknown page</dtv>; } }; return ( <di.v> <nav> onCHck={O => handleNavtgation("pageOne")}>Page One</button> onCHck={O => handleNavtgation("pageTwo")}>Page Two</button> <Ьutton <Ьutton </nav> {tsPendtng && <p>Loadtng ... </p>} {гenderPage()} </di.v> ); } export default Арр; В этом примере у нас есть два простых компонента, представляющих разные стра­ ницы в нашем SPА. Мы используем useTransttton для оборачивания обновления состояния, которое изменяет текущую страницу, гарантируя, что переход страницы будет отложен при наличии других высокоприоритетных обновлений (например, ввод данных пользователем). Вы можете подумать: "Подождите, разве переход на страницу не должен быть мгновенным, поскольку он происходит в ответ на клик пользователя?" Да, вы были бы правы; однако, если для перехода на следующую страницу требуется подгру­ зить некоторые данные с помощью Suspense, тогда переход на другую страницу мо­ жет быть отложен. Именно здесь пригодится useTransttton, поскольку он позволяет вам управлять приоритетом обновлений, связанных с навигацией, гарантируя, что взаимодействие с пользователем остается плавным и отзывчивым. Стоит отметить, что если выборка данных на следующей странице происходит в эффекте, то startTranstion не будет ждать, пока будут выбраны данные. Однако, когда вы при­ остановитесь внутри перехода, React свяжет состояние tsPendtng с выборкой дан­ ных и отображением этих данных, когда вы вернетесь обратно. В этом случае во время перехода на другую страницу состояние равно true, tsPendtng будет что позволяет нам немедленно показать индикатор загрузки пользовате­ лю в ответ на его нажатие кнопки. Как только переход будет завершен, состояние tsPendtng станет равно false, и будет отображена новая страница.
Конкурентный 241 React Погружаемся глубже Благодаря базовым знаниям о FiЬеr-архитектуре React, планировщике React, уров­ нях приоритета и механизме полос рендеринга теперь мы можем разобраться во внутренней работе хука Механизм useTransi.ti.on useTransi.ti.on. работает путем создания перехода и присвоения опреде­ ленного уровня приоритета обновлениям, выполняемым в рамках этого перехода. Когда обновление обернуто в переход, React гарантирует, что обновление будет запланировано и отрисовано в соответствии с назначенным уровнем приоритета. Вот краткий обзор шагов, связанных с использованием хука useTransi.ti.on внутри функционального компонента. 1. Импорт и вызов хука 2. Хук возвращает массив с двумя элементами: первый а второй 3. - useTransi.ti.on: - это состояние i.sPendi.ng, функция startтransi.ti.on. Функция startтransi.ti.on используется для оборачивания любого обновления со­ стояния или рендеринrа компонента, тайминr которого вы хотите контролиро­ вать. 4. Состояние 5. React i.sPendi.ng показывает, продолжается ли переход или он завершен. гарантирует, что обновления, обернутые в переход, обрабатываются с со­ ответствующим уровнем приоритета. Это достигается за счет использования планировщика и механизма полос рендеринrа для назначения обновлений и управления ими. Используя useTransi. ti.on, мы можем эффективно контролировать тайминr обновле­ ния и обеспечивать бесперебойную работу пользователей, даже когда другие об­ новления с более высоким приоритетом конкурируют за доступ к основному потоку. useDeferredVa/ue useDeferredValue - это хук React, который позволяет отложить определенные об­ новления пользовательского интерфейса на более позднее время, что особенно по­ лезно в сценариях, когда приложение сталкивается с большой нагрузкой или зада­ чами, требующими больших вычислительных затрат. Таким образом, этот хук помогает управлять приоритезацией обновлений и обеспечивает более плавные пе­ реходы и улучшенный пользовательский опыт. Во время первоначального рендеринга возвращаемое отложенное значение совпа­ дает с заданным значением. В последующих обновлениях useDeferredValue помогает поддерживать беспрепятственную работу с пользователем, сохраняя старое значе­ ние в течение более длительного времени перед обновлением до нового значения, особенно в сценариях с интенсивными вычислительными операциями. Это не при­ водит к многократному повторному рендерингу со старыми и новыми значениями, а влечет за собой контролируемое обновление до нового значения. Этот механизм
Глава 242 7 сродни стратегии stale-whi.le-гevali..date, при которой сохраняются устаревшие зна­ чения, чтобы пользовательский интерфейс оставался отзывчивым в ожидании но­ вых значений. Просматривая историю версий мы React, видим, useDeferredValue выглядела примерно так: functi.on useDeferredValue(value) { const [newValue, setNewValue] = useState(value); // // что первая реализация сохраняем молько начальное значение useEffect( О { // // обновляем возвра~аемое значение при переходе всякий раз, когда оно изменяемся, "омкладывая" его startTransition(() => { setNewValue(value); }); }, [value]); return newValue; } Давайте немного поговорим о том, что делает этот код. Сначала он устанавливает состояние (newValue) с хук useEffect переданным ему начальным значением. Затем функция ис­ пользует для наблюдения за изменениями этого значения. При обна­ ружении изменения вызывается функция startTransttion, которая имеет решающее значение для отсрочки обновления. В startTransHion состояние обновляется до нового значения Использование startTransttion сигнализирует React о том, является срочным, что позволяет более важных useDeferredValue обновлений. React Это с помощью setNewValue. что это обновление не сначала определить приоритетность других, более или менее точная модель работы сегодня, которая должна быть полезна для нашего понимания функционирования этого хука. useDeferredValue- это часть конкурентных особенностей React, которая обеспечи­ вает возможность прерывания, позволяя откладывать некоторые обновления со­ стояния. Когда компонент повторно отображается с отложенным значением, React продол­ жает показывать старое значение в течение некоторого периода времени, позволяя обновлениям с высоким приоритетом обрабатываться раньше, чем обновлениям с низким приоритетом. Это разбивает работу по рендерингу на более мелкие фраг­ менты, которые можно распределять во времени, повышая скорость отклика и обеспечивая высокий приоритет обновления (например, взаимодействие с пользо­ вателем). Высокоприоритетные обновления не задерживаются из-за обновлений с более низким приоритетом, что повышает позитивный пользовательский опыт.
Конкурентный Цель использования Основная цель React 243 useDeferredValue useDeferredValue- позволить вам отложить вывод менее важных обновлений. Это особенно полезно, когда вы хотите расставить приоритеты между более важными обновлениями, такими как взаимодействие с пользователем, и ме­ нее важными, такими как отображение обновленных данных с сервера. Применяя useDeferredValue, вы можете обеспечить более плавный пользовательский интерфейс и гарантировать, что ваше приложение останется отзывчивым даже при большой нагрузке или при выполнении сложных операций. Для того чтобы использовать кета React useDeferredValue, вам нужно импортировать его из па­ и передать значение, которое будет отложено, в качестве аргумента. За­ тем хук вернет отложенную версию значения, которую можно использовать в ва­ шем компоненте. useDeferredValue в простом useState, useDeferredValue} frOl'I "геасt"; Вот пример того, как использовать viport React, { l'lel'IO приложении: functi.on Арр() { const [searchValue, setSearchValue] = useState(""); const deferгedSeaгchValue = useDefeггedValue(seaгchValue); return ( <di.V> <i.nput type="text" value={searchValue} onChange={(event) => setSeaгchValue(event.taгget.value)} /> <SearchResutts searchValue={defeггedSearchValue} /> </di.v> ); } const SearchResults = l'1el'IO(({ searchValue }) => { // Производим поиск и Оf'/ображаем реэульмаf'/ЬI }) В этом примере у нас есть ввод для поиска и компонент SearchResults, который ото­ бражает результаты. Мы используем useDeferгedValue для отсрочки вывода резуль­ татов поиска, что позволяет приложению определять приоритеты ввода пользова­ телем и оставаться отзывчивым, дорогостоящим процессом. даже если вывод списка результатов является
Глава 244 7 Давайте разберемся в этом чуть более подробно: 1. Мы используем l'lel'lo, чтобы убедиться, что компонент не обновляется без необ­ ходимости, как мы обсуждали в предыдущих главах. 2. Когда компонент обновляется, это вызывает проблемы с производительностью, поскольку его рендеринг обходится дорого. 3. Когда мы присваиваем компоненту отложенный пропс, deferredSearchValue, по­ скольку сам пропс обновляется после более срочной работы по рендерингу, об­ новление происходит и с компонентом. Таким образом, компонент повторно отображается только тогда, когда нет более срочной работы, например обновле­ ния поля ввода текста. Кто-то может спросить: "Почему бы просто не отменить чить (throttle) значение (debounce) или не ограни­ searchValue?" Отличный вопрос. Давайте сравним эти действия. Отмена. Предполагает паузу перед обновлением списка, для ожидания, пока пользова­ тель закончит ввод текста, например, задержку в одну секунду Ограничение. Обновляет список через регулярные промежутки времени, скажем, не чаще од­ ного раза в секунду Хотя эти методы могут быть эффективны в определенных ситуациях, useDeferredValue представляется более подходящим решением для оптимизации рендеринга, по­ скольку оно легко адаптируется к возможностям производительности устройства пользователя и не вызывает случайные задержки. Ключевое отличие в случае useDeferredValue заключается в динамическом подходе к задержкам. Это устраняет необходимость в установке фиксированного времени за­ держки. На высокопроизводительных устройствах, таких как мощный ноутбук, за­ держка при повторной визуализации незаметна и происходит практически мгно­ венно. И наоборот, на более медленных устройствах задержка рендеринга регулируется в соответствии с производительностью, и список обновляется в ответ на ввод с небольшой задержкой, пропорциональной скорости устройства. Кроме того, useDeferredVa lue обладает значительным преимуществом в возможно­ сти прерывать отложенные повторные рендеринги. В сценариях, когда React обра­ батывает большой список, а пользователь совершает новое нажатие клавиши, React может приостановить повторную визуализацию, отреагировать на новый ввод дан­ ных, а затем возобновить процесс рендеринга в фоновом режиме. Это отличается от отмены и ограничений, которые, несмотря на задержку обновления, все равно мо­ гут привести к сбоям в работе, поскольку блокируют интерактивность во время рендеринга. Тем не менее отмена и ограничение по-прежнему полезны в сценариях, не связан­ ных напрямую с рендерингом. Например, они могут быть эффективны для сниже­ ния частоты сетевых запросов. Эти методы также могут быть использованы в соче­ тании с useDeferredValue для комплексной стратегии оптимизации.
Конкурентный Исходя из всего useDeferredVa lue этого, мы в приложениях видим несколько преимуществ React 245 использования React. Улучшена скорость отк.шка. В примере, когда пользователь вводит текст в поле поиска, поле ввода немед­ ленно обновляется, а результаты отображаются с задержкой. Если пользователь быстро вводит пять символов подряд, поле ввода обновляется после ввода каж­ дого символа, а searchResults отображается только один раз, после того как поль­ зователь прекратит вводить текст. Для символов отображение 1--4 searchResults прерывается вводом новых значений. Дек.тративная расстановка приоритетов. useDeferredVa lue обеспечивает простой и декларативный способ управления при­ оритезацией обновлений в вашем приложении. Инкапсулируя логику отсрочки обновлений внутри хука, вы можете сохранить чистоту кода компонента и сфо­ кусироваться на важных аспектах вашего приложения. Более эффектитюе использование рес~vрсов. useDeferredValue Благодаря отсрочке менее важных обновлений хук позволяет вашему приложению более эффективно использовать доступные ресурсы. Это может помочь снизить вероятность возникновения "узких мест" и повысить об­ щую производительность вашего приложения. Когда применять useDeferredVa lue useDeferredValue наиболее полезен в ситуациях, когда вашему приложению необходи­ мо отдавать предпочтение определенным обновлениям перед другими. Вот несколько распространенных сценариев, в которых можно использовать ♦ поиск или фильтрация больших наборов данных; ♦ визуализация сложных компонентов или анимаций; ♦ обновление данных с сервера в фоновом режиме; ♦ useDeferredValue: выполнение дорогостоящих вычислительных операций, которые могут повлиять на взаимодействие с пользователем. Давайте рассмотрим пример, в котором useDeferredVa lue может быть особенно по­ лезным. Представьте, что у нас есть большой список элементов, которые мы хотим отфильтровать на основе пользовательского ввода. Фильтрация длинного списка может потребовать больших useDeferredValue может помочь vipoгt React, { l'Jef'IO, useState, вычислительных затрат, поэтому useМef'10, useDeferredValue} fгOPI "геасt"; functi.on Арр() { const [fi.lter, setFi.lter] = useState(""); const deferredFi.lter = useDeferredValue(fi.lter); const i.teмs использование сохранить отзывчивость приложения: = useMeмo(() => generatelargeli.stOflteмs(), []);
246 Глава 7 const ftlteredlteмs = useMeмo(() => { return tteмs.ftlter((tteм) => tteм.tncludes(deferredFtlter)); }, [tteмs, deferredFtlter]); return ( <di.v> <i.nput type="text" value={Hlter} onChange={(event) => setFtlter(event.target.value)} /> <ItE!l'ILi.st tteмs={ftlteredlteмs} /> </di.v> ); } const Iteмltst = мемо(({ // Рендеринг списка tteмs }) => { }); functi.on generatelargeLtstOfiteмs() { // Генерация болЬlJJого списка для примера } В этом примере мы используем useDeferredValue для отсрочки вывода отфильтро­ ванного списка. По мере того, как пользователь вводит данные для фильтра, отло­ женное значение обновляется реже, что позволяет приложению отдавать приоритет пользовательскому вводу и оставаться отзывчивым. Хуки useMeмo используются для запоминания элементов и массивов fHterediteмs, предотвращая ненужные повторные визуализации и вычисления. Это еще больше повышает производительность приложения. Когда не следует использовать Хотя в определенных сценариях useDeferredValue useDeferredValue может быть полезным, важно учи­ тывать компромиссы. А именно, при отсрочке обновления существует вероятность того, что данные, показываемые пользователю, могут немного устареть. Хотя это обычно приемлемо для менее важных обновлений, необходимо учитывать послед­ ствия отображения устаревших данных для пользователей. При принятии решения о том, следует ли использовать useDeferredValue или нет, нужно задать себе вопрос: "Является ли это обновление результатом введения дан­ ных пользователем?"
Конкурентный React называется React 1 247 просто так: он позволяет нашим веб-приложениям реаги­ R~act не ровать на различные события. Все, что побуждает пользователя ожидать реакции, не должно откладываться. При этом все остальное должно быть отложено. Хотя использование useDeferredValue может значительно повысить скорость реаги­ рования вашего приложения при увеличенной нагрузке, его не следует рассматри­ вать как волшебную палочку. Всегда помните, что лучший способ повысить произ­ водительность - это написать эффективный код и избежать ненужных вычислений. Проблемы с конкурентным рендерингом Конкурентный рендеринг, хотя и обеспечивает эффективное и отзывчивое взаимо­ действие с пользователем, создает новые проблемы для разработчиков. Основная проблема заключается в том, что трудно определить правильный порядок обработ­ ки обновлений, что может привести к неожиданному поведению и ошибкам. Одна из таких ошибок называется разрывом (tearing), когда пользовательский ин­ терфейс становится несогласованным из-за того, что обновления обрабатываются не по порядку. Это может произойти, когда компонент зависит от некоторого зна­ чения, обновляемого во время его отрисовки, что приводит к отрисовке приложе­ ний с противоречивыми данными. Давайте немного углубимся в это. Разрыв Разрыв - это ошибка, возникающая, когда компонент зависит от некоторого со­ стояния, которое обновляется, пока приложение все еще выполняет рендеринг. Для того чтобы понять это, давайте сравним синхронный рендеринг с конкурентным рендерингом. В синхронном мире React просматривал бы дерево компонентов и отображал бы их один за другим, сверху вниз. Это гарантирует, что состояние приложения остается неизменным на протяжении всего процесса рендеринга, поскольку каждый компо­ нент отображается в последнем состоянии. Рассмотрим такой пример: 'U'lf)Oгt { useState, useSyncExteгnalStore, useTransi.ti.on} // Внешнее сос~ояние tet count = 0; set!nterval(() => count++, 1); defautt funcrton Арр() { const [nal'IE!, setNal'IE!] = useState(""); const [i.sPendi.ng, startTransi.ti.on] = useTransi.ti.on(); ехрогt const updateNal'IE! = (newVal) startTransi.ti.on(() => { => { fгOPI "геасt";
248 Глава 7 setNaмe(ne'INal); }); }; return <di.v> <i.nput onChange={(e) => {isPending && <di.v>Loading ... </di.v>} value={naмe} <Ul> <li.> <Expensi.veCOl'll)Onent </ti.> <li.> <Expensi.veCOl'lponent </ti.> <li.> <Expensi.veCOl'lponent </li.> <li.> <Expensi.veCOl'lponent </li.> <ti.> <Expensi.veCOl'lponent </ti.> </ul> </di.v> updateNaмe(e.target.value)} /> /> /> /> /> /> ); } const ExpensiveC0111ponent = () => { const now = perfor~ance.now(); whi.te (perfor~ance.now() - now < 100) { // Ничего не делаем, npocPlo ждем. } return <>Expensive count is {count}</>; }; В самом верху нашего приложения у нас есть count - переменная, которую мы ус­ танавливаем глобально и постоянно обновляем с помощью setinterval вне цикла
Конкурентный рендеринга React, React 249 чтобы мы могли имитировать ошибку разрыва, обновляя ее во время рендеринга приложения. Поскольку рендеринг конкурентный и может пре­ рываться, возможно, что Expensi.veCOl'lponent будет отображаться с разными значе­ ниями count, что приведет к отображению несогласованных данных для пользова­ теля или их разрыву. Мы ожидаем увидеть несогласованные Expensi.veCOl'lponent, поскольку React значения count, отображаемые внутри "останавливает" рендеринг при вводе пользова­ телем данных, чтобы определить приоритет более срочного обновления, такого как обновление поля ввода текста, тем самым оставляя устаревшее значение count в Expensi.veCOl'lponent, но не каждый раз, только иногда. В нашем примере отображается поле ввода текста и список из пяти экземпляров Expensi.veCOl'lponent. Эти компоненты специально не запоминаются, чтобы проиллю­ стрировать суть, поскольку они вызывают проблемы с производительностью, а нам нужны эти проблемы, чтобы идентифицировать разрывы в целях их понимания. В реальном мире вам захочется обернуть Expensi.veC0111ponent в React.l'lel'IO. Здесь мы намеренно избегаем этого, чтобы продемонстрировать разрывы, которых вы захо­ тите избежать в своем приложении. Рендеринг Expensi.veCOl'lponent занимает много времени, имитируя вычислительный процесс дорогостоящей операции. значение переменной Expensi.veCOl'lponent также отображает текущее count, которое увеличивается каждую миллисекунду и считы­ вается из внешнего хранилища, в данном случае из глобального пространства имен. Если мы запустим этот пример, то увидим, что для пяти экземпляров Expensi.veCOl'lponent, которые мы визуализируем, после ввода нескольких нажатий клавиш компоненты Expensi.veCOl'lponent будут отображаться с разными значениями count. Это происходит потому, что Expensi.veC0111ponent визуализируется пять раз, и каждый раз, когда он визуализируется, значения count отличаются. Поскольку React ото­ бражает компоненты конкурентно, возможно, что Expensi.veC0111ponent будет отобра­ жаться с разными значениями count, что приведет к несогласованным данным, ото­ бражаемым пользователю. Это называется разрывом, и это ошибка, которая может возникнуть, когда компо­ нент зависит от некоторого состояния, обновляемого во время процесса рендеринга приложения. В этом случае Expensi.veCOl'lponent зависит от переменной count, которая обновляется во время выполнения рендеринга компонента, что приводит к ренде­ рингу приложения с несогласованными данными. При разрыве для пяти экземпля­ ров • • • • • Expensi.veCOl'lponent мы видим следующий результат: Expensi.ve count i.s 568; Expensi.ve count i.s 568; Expensi.ve count i.s 569; Expensi.ve count i.s 569; Expensi.ve count i.s 570.
Глава 250 7 Все верно, потому что отображаются более ранние экземпляры компонента, обнов­ ленное значение count сбрасывается/фиксируется в DOM, а более ранние экземпля­ ры продолжают отображаться и выдаются (сбрасываются, обновляются) с более новыми значениями count. Ситуация не критична, потому что React в конечном итоге отображает согласован­ ное состояние. Более серьезная проблема возникает, когда у вас есть что-то вроде: <UserDetatls id={user.td} /> Этот код, если пользователь будет удален из глобального хранилища между отри­ совками страницы, приведет к внезапной ошибке, которая может удивить пользо­ вателя. Вот почему разрыв является проблемой. Для того чтобы решить эту проблему, useSyncExternalStore. React предоставляет хук под названием Давайте разберемся с этим хуком. useSyncExterna/Store useSyncExterna lStore - это хук React, который позволяет синхронизировать внеш­ нее состояние с внутренним состоянием вашего приложения. Это особенно полезно при выполнении дорогостоящих вычислительных операций, которые могут привес­ ти к разрыву при useSyncExternalStore неправильной обработке. Термин "синхронизация" в имеет двойное значение. Он означает и "синхронизировать", и "синхронный": он запускает синхронное обновление при изменении хранилища. Механизм использования внешнего хранилища для синхронизации имеет следую­ щую сигнатуру: const value = useSyncExternalStore(store.subscгtЬe, store.getSnapshot); store.subscrtbe Это функция, которая получает функцию обратного вызова в качестве своего первого и единственного аргумента. Внутри этой функции вы можете подпи­ саться на изменения во внешнем хранилище и вызывать функцию обратного вы­ зова при каждом изменении в хранилище. Обратный вызов можно рассматри­ вать как вызов, позволяющий оперативно отреагировать на повторную отрисовку компонента с новым значением. Ожидаемый результат этой функ­ ции - функция очистки, которая отменяет подписку на хранилище. Типичная функция подписки subscгtЬe выглядит следующим образом: const store = { subscrtbe(rerender) { const newData = getNewData().then(rerender); return () => { // о~писа~ься как-нибудь }; }, };
Конкурентный React 1 251 Простым вариантом использования для этого была бы подписка на события браузера, такие как изменение размера resi.ze или прокрутка scroll. Обновление компонента при возникновении этих событий можно отразить следующим образом: const store = { subscri.Ьe(rerender!ГТ1edi.ately) { rerenderl1'11'1edi.ately); () => { wi.ndow.re111oveEventli.stener("resi.ze", rerenderll'll'ledi.ately); wi.ndow.addEventli.steneг("гesi.ze", гetuгn }; }, }; Теперь наши компоненты React будут повторно отрисовываться при изменении размера окна браузера. Однако как они получат новое значение? Вот здесь появ­ ляется второй аргумент для useSyncExternalStore. store.getSnapshot Функция, которая возвращает текущее значение внешнего хранилища. Она вы­ зывается при каждом отображении компонента, а возвращаемое значение ис­ пользуется для обновления внутреннего состояния компонента. Эта функция вызывается синхронно, поэтому она не должна выполнять никаких асинхронных операций или иметь каких-либо побочных эффектов. Более того, эта функция гарантирует, что состояние во время рендеринга будет одинаковым для несколь­ ких экземпляров компонента. Следуя нашему примеру с изменением размера окна, мы получим текущий раз­ мер окна следующим образом: const store = { subscгi.be(i.medi.atelyReгendeгSynchronously) wi.ndow.addEventli.stener("resi.ze", i.medi.atelyRerenderSynchronously); гetuгn () => { wi.ndow.re111oveEventli.stener("resi.ze", i.l'll'ledi.atelyRerendeгSynchronously); }; }, getSnapshot() { гetuгn { wi.dth: wi.ndow.i.nnerWi.dth, hei.ght: wi.ndow.i.nnerHei.ght, }; }, }; {
7 Глава 252 Объект с параметрами { wi.dth, hei.ght } - зто моментальный снимок текущего useSyncExterna lStore. Затем мы можем ис­ состояния окна, и именно его вернет пользовать зтот объект в нашем компоненте с уверенностью, что при конку­ рентном рендеринге его состояние всегда будет одинаковым. Откуда у нас такая уверенность? i.1'11'1edi.atelyRerenderSynchronously не позволяет React отложить ее. Это потому, что функция запускает синхронную повторную отрисовку и Это ключ к решению проблемы разрыва. Теперь давайте посмотрим, как мы можем использовать useSyncExternalStore для решения проблемы разрыва в нашем предыдущем примере. Если мы помним, мы видели список экземпляров значениями count Expensi.veCo1'1ponent, которые отображались с разными из-за разрыва. Сейчас мы посмотрим, как можно это исправить, используя синхронизацию с внешним хранилищем useSyncExterna lStore. Мы не хотим подписываться на хранилище и за11ускать повторную отрисовку при появлении обновлений. Вместо этого нам нужно согласованное состояние повтор­ ной отрисовки subscri.be из-за пользовательского ввода. Таким образом, наша функция будет пустой, но для получения согласованного состояния мы будем ис­ пользовать функцию getSnapshot, чтобы получить текущее значение параметра и вернуть его: const store = { subscri.be() {}, getSnapshot() { гetuгn count; }, }; Вот как будет выглядеть наш предыдущий пример с useSyncExternalStore: l.l'lport { useState, useSyncExternalStore, useTransi.ti.on} frOl'I "геасt"; let count = 0; setlnterval(() => count++, 1); default function Арр() { const [nal'le, setNa1'1e] = useState(""); const [, startTransi.ti.on] = useTransi.ti.on(); ехрогt const updateNal'1e = (newVal) startTransi.ti.on(() => { setNal'le(newVal); }); }; гetuгn <dtv> ( => { count
Конкурентный React 253 <input value={narrie} onChange={(e) => updateNarrie(e.target.value)} /> <Ul> <li.> <Expensi.veCOl'll)Onent /> </li.> <li.> <Expensi.veCOl'll)Onent /> </li.> <li.> <Expensi.veCOl'lpOnent /> </li.> <li.> <Expensi.veCOl'll)Onent /> </li.> <li.> <Expensi.veCOl'll)Onent /> </li.> </ul> </di.v> ); } const Expensi..veCOP1ponent = () => { // Вмесf'lо глобального Чf'/енuя count / / l,t,/ применим хук, Чf'/обы обеспечuf'lь согласованное сосf'/ояние const consi..stentCount = useSyncExternalStore( о => {}, () => count ); const now = perfor~ance.now(); whi.le (perfor~ance.now() - now < 100) { // Ничего не делаем } гetuгn <>Expensi..ve count i..s {consi..stentCount}</>; }; Теперь, если мы запустим этот пример, мы увидим, что компоненты отображаются с одинаковым значением параметра Expensi..veCo~ponent count, что предотвращает воз­ useSyncExternalStorage обеспе- никновение разрыва. Это связано с тем, что функция
Глава 254 7 чивает согласованность состояния во время рендеринга во всех экземплярах ком­ понента. Мы не используем функцию subscгlЬe, потому что ее цель - сообщить React, когда следует выполнить повторную отрисовку с последним состоянием, но в нашем слу­ чае мы просто хотим, чтобы состояние было согласованным во всех отрисовках. Мы используем функцию count getSnapshot, чтобы получить текущее значение параметра и вернуть его, обеспечивая согласованность состояния во время рендеринга нескольких экземпляров компонента. Таким образом, мы можем использовать мы разрыва в нашем предыдущем useSyncExternalStore примере, гарантируя, для решения пробле­ что состояние во время рендеринга будет согласовано для нескольких экземпляров компонента. Хорошо. Это гарантирует, что при вводе текста и повторной отрисовке компонент Expensi.veCoJ11ponent будет иметь то же значение count, что и другие экземпляры Expensi.veCol'1ponent. Это предотвращает разрыв, но что, если мы захотим обновить count внутри Expensi.veCOP1ponent с тем же интервалом, с которым мы обновляем count за пределами Expensi.veCOP1ponent? Мы просто создаем для него хранилище, которое следует тем же правилам обнов­ ления: 'U'lport { useState, useSyncExternalStore, useTransi.ti.on} frOl'I let count = 0; setlnterval(() => count++, 1); const store = { subscri.Ьe(forceSyncRerender) { // Если count изменился ... forceSyncRerender(); }, getSnapshot() { return count; }, }; default functi.on Арр() { const [nal'le, setNa1'1e] = useState(""); const [, startTransi.ti.on] = useTransi.ti.on(); ехрогt const updateNa/11e = (newVal) startTransi.ti.on(() => { setNa/11e(newVal); }); }; => { "геасt";
Конкурентный React 255 return ( <d"i.v> <'\.nput value={na111e} onChange={(e) => updateNa111e(e.target.value)} /> <ul> <l'\.> <Expens'\.veCOP1ponent /> </Н> <Н> <Expens'\.veCOP1ponent /> </Н> <l'\.> <Expens'\.veCOP1ponent /> </Н> <Н> <Expens'\.veCOP1ponent /> </Н> <l'\.> <Expens'\.veCOP1ponent /> </l'\.> </ul> </d'\.v> ); } const ExpensiveC01'1ponent = () => { // // Вмесrrю глобального счиf'lывания count №1>1 будем использоваf'lь хук для обеспечения согласованного сосf'/ояния const consistentCount = useSyncExternalStore( store.subscribe, store.getSnapshot ); const now = perfor~ance.now(); wh'\.le (perfor~ance.now() - now < 100) { // Ничего не делаем } return <>Expensive count is {consistentCount}</>; }; Теперь, всякий раз, когда новым значением count, count ExpensiveCo~ponent будет отображаться с одинаковое значение count во всех экземпля- изменяется, и мы увидим
Глава 256 7 рах Expensi.veCol'1ponent. Сама логика обнаружения изменений может быть как про~ стой, так и сложной, какой вы хотите ее видеть, но главное в том, что мы понимаем механизмы, с помощью которых выполняет свои основные useSyncExternalStore функции, а именно: ♦ обеспечивает согласованное состояние при конкурентном рендеринге; ♦ принудительно выполняет синхронный повторный рендеринг при изменении хранилища. Теперь, когда мы поняли, как useSyncExternalStore работает и решает проблему раз­ рыва, мы научились разбираться не только в конкурентном рендеринге в React, но и в том, как решать некоторые возникающие при этом проблемы. Это важная область для приложения ваших усилий в качестве разработчика React. Это было довольно глубокое погружение, но мы почти закончили. Давайте подве­ дем некоторые итоги. Обзор главы Этот обстоятельный разговор был посвящен глубокому изучению конкурентной работы React, затрагивающей множество аспектов, включая FiЬеr-согласователь, планирование, useTransi.ti.on и отсрочку обновлений, полосы рендеринга и новые хуки - useDeferredVa lue. Мы начали с обсуждения FiЬеr-согласователя, ядра механизма конкурентного рен­ деринга React. Именно он дает возможность фреймворку разбивать работу на более мелкие фрагменты и управлять приоритетом выполнения, что позволяет React быть "прерываемым" и поддерживать конкурентный рендеринг. Это в значительной сте­ пени повышает способность React плавно работать со сложными высокопроизводи­ тельными приложениями, гарантируя, что взаимодействие с пользователем остает­ ся отзывчивым даже во время интенсивных вычислений. Затем мы перешли к концепции планирования и отсрочки обновлений, которая, по сути, позволяет другими. React React устанавливать приоритет одних обновлений состояния над может откладывать обновления с более низким приоритетом в поль­ зу обновлений с более высоким приоритетом, тем самым обеспечивая бесперебой­ ную работу пользователей даже при большой нагрузке. В качестве примера было приведено приложение для чата, в котором обновления входящих сообщений были запланированы разумно и отображались без блокировки пользовательского интер­ фейса. Далее обсуждение перешло к полосам рендеринга, центральной концепции конку­ рентных особенностей React. Полосы рендеринга - ло механизм, который React использует для присвоения приоритета обновлениям и эффективного управления их выполнением. В этом заключен секрет того, как React решает, какие обновления являются срочными и требуют немедленной обработки, а какие можно отложить на более поздний срок. В подробном объяснении говорилось о том, как для эффектив-
Конкурентный 257 React ной обработки нескольких приоритетов полосы рендеринга используют растровые маски. Затем мы углубились в изучение новых хуков, введенных для конкурентных опера­ ций в React, а именно useTransi.ti.on и useDeferredValue. Эти хуки предназначены для обработки переходов и обеспечения более плавного взаимодействия с пользовате­ лем, особенно при выполнении операций, которые занимают значительное количе­ ство времени. Сначала был рассмотрен механизм useTransi.ti.on, который позволяет реагировать на переход между состояниями таким образом, чтобы обеспечить отзывчивый пользо­ вательский интерфейс, даже если для подготовки нового состояния требуется неко­ торое время. Другими словами, он позволяет отложить обновление до следующего цикла рендеринга, если компонент в данный момент отображается. Мы также обсудили механизм useDeferredValue, который откладывает обновление менее важных частей компонента, тем самым предотвращая неудобства при работе с пользователем. По сути, он позволяет React "удерживать" предьщущее значение немного дольше, если подготовка нового значения занимает слишком много времени. Наконец, мы углубились в проблемы конкуренции в чили, как useSyncExterna lStore React, включая разрыв, и изу­ может помочь поддерживать согласованность со­ стояния при нескольких конкурентных отрисовках. На протяжении всего разговора повторяющейся темой было объяснение того, "что" и "почему" стоит за стратегиями React по управлению сложными, динамичными приложениями с большими объемами вычислений и как разработчики могут ис­ пользовать эти стратегии для обеспечения плавного и отзывчивого взаимодействия с пользователями. Проверьте ваши знания Давайте зададим себе несколько вопросов, чтобы проверить наше понимание кон­ цепций, изложенных в этой главе: 1. Что такое FiЬеr-согласователь React и как он способствует работе со сложными высокопроизводительными приложениями? 2. Объясните концепцию планирования и отсрочки обновлений в React. Как это помогает поддерживать бесперебойную работу пользователей даже при боль­ шой нагрузке? 3. Что такое полосы рендеринга в React и как они управляют выполнением обнов­ лений? Можете ли вы описать, как полосы рендеринга используют растровые маски для обработки нескольких приоритетов? 4. Какова цель использования хуков useTransi.ti.on и useDeferredValue в React? Опи­ шите ситуацию, в которой каждый из них был бы полезен. 5. Когда может быть неуместно использовать useDeferredValue? компромиссы, связанные с использованием этого хука? Каковы некоторые
Глава 258 7 Что дальше? Теперь, когда у вас есть глубокое понимание конкурентных особенностей React и его внутренней работы, вы хорошо подготовлены к тому, чтобы в полной мере ис­ пользовать его потенциал при создании высокопроизводительных приложений. В главе 8 React, такие как мы рассмотрим различные популярные фреймворки, созданные поверх Next.js и Remix, которые еще больше упрощают процесс разработ­ ки, предоставляя лучшие практики, соглашения и дополнительные функции. Эти фреймворки предназначены для того, чтобы помочь вам с легкостью создавать сложные приложения, решая многие общие проблемы, такие как серверный ренде­ ринг, маршрутизация и фрагментация кода. Используя достоинства этих фреймвор­ ков, вы сможете сосредоточиться на расширении возможностей вашего приложе­ ния, обеспечивая при этом оптимальную производительность и удобство для пользователей. Впереди вас ждет подробное знакомство с этими мощными платформами. Вы уз­ наете, как создавать масштабируемые, производительные и многофункциональные приложения, используя React и его экосистему.
ГЛАВА 8 Фреймворки За время нашего знакомства с React мы открыли для себя широкий спектр функций и принципов, которые обеспечивают его мощь и универсальность. В предыдущей главе мы углубились в увлекательный мир асинхронного тавляет нам такие инструменты, как useTransttton и React, который предос­ useDeferredValue, позволяющие создавать высокочувствительные и удобные для пользователя интерфейсы. Мы ис­ следовали, как для достижения оптимальной производительности эти инструменты используют сложные механизмы планирования и расстановки приоритетов React, которые стали возможны благодаря FiЬеr-согласователю. Поскольку в этой главе мы углубимся в область фреймворков React, понимание этих асинхронных моделей имеет решающее значение. React сам по себе - невероятно мощный инструмент, но по мере усложнения при­ ложений мы часто сталкиваемся с тем, что просто повторяем шаблоны вместо на­ хождения более рациональных решений общих задач. Именно здесь на помощь приходят фреймворки. Фреймворки React - это программные библиотеки или на­ React и предоставляющие дополнительные боры инструментов, созданные поверх абстракции для более эффективного решения общих задач и применения передо­ вых практик. Зачем нам нужен фреймворк Хотя React предоставляет базовые блоки для создания интерактивных пользова­ тельских интерфейсов, многие важные архитектурные решения остаются на усмот­ рение разработчиков. В этом отношении доставляя разработчикам React необходимую не дает однозначного решения, пре­ гибкость в структурировании своих приложений. Однако по мере масштабирования приложений эта свобода может превратиться в проблему. Возможно, вам придется изобретать велосипед заново, снова и снова сталкиваясь с такими распространенными задачами, как маршрути­ зация, выборка данных и рендеринг на стороне сервера. Именно здесь на помощь приходят фреймворки React. Они предоставляют предо­ пределенную структуру и решения распространенных проблем, позволяя разработ­ чикам сосредоточиться на уникальности своего приложения, а не вязнуть в шаб-
260 Глава 8 лонном коде. Это может значительно ускорить процесс разработки и улучшить ка­ чество кодовой базы за счет следования лучшим практикам, которые предлагает фреймворк. Для того чтобы полностью понять это, давайте попробуем написать наш собствен­ ный минимальный фреймворк. Для этого нам нужно определить несколько ключе­ вых функций, которые мы получим из создаваемого нами фреймворка и которые не так просто написать, используя чистый React. Для краткости выделим три специ­ фические функции, которые мы хотим получить от фреймворка. Стоит отметить, что фреймворки делают гораздо больше, но этот список поможет нам сформиро­ вать основу для интересного обсуждения: ♦ серверный рендеринг; ♦ маршрутизация; ♦ выборка данных. Давайте возьмем уже существующее воображаемое приложение React и постепенно добавим эти функции, чтобы понять, что для нас делают фреймворки. Приложение React, вокруг которого - i.ndex.js - Li.st.js - Detai.l. j s - di.st/ - cli.entBundle.js мы построим фреймворк, имеет следующую структуру: Вот как выглядит каждый файл: // index.js i.rlpoгt 'U'lpoгt i.rlpoгt React frOl'I "react"; { createRoot} frOl'I "react-dOl'l/cli.ent"; Router frOl'I "./Router"; const root = createRoot(docul'lent); const paraмs = new URLSearchParaмs(); const thi.ngld = paraмs.get("i.d"); root.render( wi.ndow.locati.on.pathnal'le === "/"? <L"ist /> ); // List.js ехрогt const Li.st =О=> { const [thi.ngs, setThi.ngs] = useState([]); <DetaH thi.ngld={thi.ngld} />
Фреймворки const [requestState, setRequestState] = useState("t.nt.tt.al"); const [еггог, setError] = useState(null); useEffect(() => { setRequestState("loadt.ng"); fetch("https://api..c0111/get-lt.st") .then((r) => r.json()) .then(setTht.ngs) . then(() => { setRequestState("success"); }) .catch((e) => { setRequestState("error"); setError(e); }, }); []); гetuгn ( <di.v> <ul> {tht.ngs.мap((thi.ng) <li. => ( key={thi.ng.i.d}>{tht.ng.laЬel}</li.> ))} </Ul> </di.v> ); }; // Detail.js const Detat.l = ({ thi.ngld }) => { const [tht.ng, setThi.ng] = useState([]); const [requestState, setRequestState] = useState("i.ni.ti.al"); const [еггог, setError] = useState(null); ехрогt useEffect(() => { setRequestState("loadt.ng"); fetch("https://api..c0111/get-tht.ng/" + thi.ngld) .then((r) => r.json()) .then(setтht.ngs) . then( О => { 1 261
Глава 262 8 setRequestState("success"); }) .catch((e) => { setRequestState("error"); setError(e); }); }, []); гetuгn ( <di.v> <h1>The thi.ng!</h1> {thi.ng} </di.v> ); }; Существует несколько проблем, которые оказывают влияние на все приложения React с клиентским рендерингом. Мы отправляем пользователю пустую страницу, содержащую только код для за­ грузки, затем анализируем и выполняе,w код JavaScript. Пользователь видит пустую страницу, пока не заработает JavaScript, который за­ грузит наше приложение. Если пользователь работает в поисковой системе, он может ничего не увидеть. Если поисковый робот не поддерживает JavaScript, поисковая система не будет индексировать наш веб-сайт. Мы начинаем получать данные слишком поздно. Наше приложение становится жертвой так называемого сетевого водопада, ко­ торый возникает, когда сетевые запросы выполняются последовательно и замед­ ляют работу приложений. Приложению приходится выполнять множество за­ просов к серверу для обеспечения хотя бы базовой функциональности. Приложение здесь работает примерно следующим образом: • загружает, анализирует и выполняет JavaScript-кoд; • отображает и фиксирует компоненты • useEffect начинает • выборку данных; отображает и фиксирует спиннеры и т. д.; • useEffect завершает • React; выборку данных; отображает и фиксирует данных. Всего этого можно избежать, если мы будем предоставлять страницу с данными прямо в браузер, т. е. если мы отправим НТМL-разметку, как описано в главе посвященной серверному React. 7,
Фреймворки 1 263 Наш маршрутизатор работает исключителыю на клиентской основе. Если браузер запрашивает страницу 404, https://our-app.com/detail?thingld=24, сервер выдает потому что на сервере нет такого файла. Обычный способ устра­ - при обнаружении ошибки 404 отображать НТМL-файл, JavaScript и использует маршрутизатор на стороне клиента. нения этой проблемы который загружает Эта уловка не работает в поисковых системах или средах, где поддержка JavaScript ограничена. Фреймворки помогают решить эту и многие другие проблемы. Давайте рассмот­ рим, как именно они это делают. Серверный рендеринг Начнем с того, что фреймворки обычно предоставляют нам серверный рендеринг, что называется, "из коробки". Для того чтобы добавить серверный рендеринг в на­ ше приложение, нам нужен сервер. Мы можем написать его сами, используя пакет, подобный Express.js. Затем мы развернем этот сервер и приступим к работе. Давай­ те рассмотрим код, который будет работать на таком сервере. Прежде чем мы начнем, пожалуйста, имейте в виду, что мы используем renderToStгlng только для простоты и иллюстрации базовых механизмов, лежащих в основе реализации фреймворком этих возможностей. В реальных производствен­ ных условиях почти всегда лучше полагаться на более мощные асинхронные серверного рендеринга, такие как renderT0Pi.peableStrear1, как это описано в главе После этого замечания давайте посмотрим на следующий код: // ./seгveг.js 'U'lport express frOl'I "express"; 'U'lport { renderToStгlng} frOl'I "react-dor1/serveг"; // Описано в главе 'U'lport { Li.st} frOl'I "./Li.st"; 'U'lport { Detai.1} frOl'I "./Detai.1"; const арр = express(); app.use(express.stati.c("./di.st")); // // Получим cl'lal'lичecкue файлы, l'lйкие как client JS const createlayout = (chHdren) => '<htr111ang="en"> <head> <ti.tle>My page</ti.tle> </head> <Ьоdу> ${chHdгen} 6 API 6.
Глава 264 8 <scrt.pt src="/clt.entBundle.js"></scrt.pt> </Ьоdу> <htмl>.; app.get("/", => { (геq, геs) res.setНeader("Content-Type", "text/htмl"); res.end(createlayout(renderToStrt.ng(<Ltst />))); }); app.get("/detaH", (геq, геs) => { res.setНeadeг("Content-Type", "text/htмl"); res.end( createlayout(гenderToStrt.ng(<Detatl tht.ngld={req.paraмs.tht.ngld} />)) ); }); app.lt.sten(3000, () => { console.t.nfo("App t.s lt.stent.ng!"); }); Этот код- все, что нам нужно, чтобы добавить серверный рендеринг в наше при­ ложение. Обратите внимание, что indexjs на стороне клиента имеет собственный клиентский маршрутизатор, и мы, по сути, просто добавили еще один маршрутиза­ тор для сервера. Фреймворки предлагают изоморфные маршрутизаторы, которые работают как на клиенте, так и на сервере. Маршрутизация Хотя с этим сервером все в порядке, он плохо масштабируется: для каждого нового маршрута нам придется вручную добавлять дополнительные вызовы req.get. Да­ вайте сделаем наш сервер немного более масштабируемым. Мы можем решить эту задачу несколькими способами, например с помощью объекта конфигурации, кото­ рый сопоставляет маршруты с компонентами, или с помощью маршрутизации на основе файловой системы. В образовательных целях (и, честно говоря, для развле­ чения) давайте рассмотрим маршрутизацию на основе файловой системы. Именно здесь становятся более понятными обоснование и механизм, лежащий в основе со­ глашений и мнений о таких фреймворков, как ./pages, а все имена файлов в данном каталоге при этом становятся путями маршру­ наш сервер с которым может все полагаться становясь более масштабируемым. страницы Когда мы применяем согла­ с тизатора, соответствии Next.js. шение, на такое должны находиться соглашение как в каталоге на допущение,
Фреймворки 1 265 Давайте проиллюстрируем сказанное на примере. Во-первых, мы расширим нашу структуру каталогов. Новая структура каталогов выглядит следующим образом: - i.ndex.js - pages/ - li.st.js - detai.l.js - di.st/ - cli.entBundle.js Теперь мы можем предположить, что все файлы в каталоге pages становятся мар­ шрутами. Давайте обновим наш сервер, чтобы он соответствовал этому: // ./sегvег. js fror1 "express"; { joi.n} frOl'I "path"; { renderToStri.ng} fror1 vipoгt expгess vipoгt 1J11poгt "гeact-d0111/server"; // Описано в главе б const арр = express(); app.use(express.stati.c("./di.st")); // Получим СРЮРluческие // Рlйкие как с l ient JS const createlayout = (chi.ldren) => '<ht111l lang="en"> <head> <ti.tle>Мy page</ti.tle> </head> файлы, <Ьоdу> ${chi.ldren} <scri.pt src="/cli.entBundle.js"></scri.pt> </Ьоdу> <ht111l>'; app.get("/:route", async (req, геs) => { // Имnо{)f'lируем компоненf'IЫ МйPIIIPYPIUЗaPIOpa из каf'ЮЛога pages const exportedStuff = await i.111port( joi.n(process.cwd(), "pages", гeq.para111s.route) ); // М,J болЫ1Jе не можем использоваРlь именованный экспо{)f'I, // nОР1ому ЧРIО нам нужна предсказуемосР1ь. // ПоЭР/ому /111,/ выбираем эксnо{)РI по умолчанию. // ". defau l t" сР1андарР1изирован, и nоЭР1ому /111,/ const Page = exportedStuff.default; можем на него положuРlься.
266 Глава 8 // МожеРI быРlь, ~ сумеем const props = req.query; вывесР1u nponc из сР1роки запроса? res.setHeader("Content-Type", "text/htr,l"); res.end(createlayout(renderToStrtng(<Page { ... props} }); />))); app.ltsten(3000, () => { console.tnfo("App ts Hstentng!"); }); Теперь наш сервер масштабируется намного лучше благодаря принятому нами но­ вому соглашению о каталоге ./pages! Отлично! Однако теперь мы вынуждены экс­ портировать компоненты по умолчанию для каждой страницы, поскольку наш под­ ход является более общим, и в противном случае было бы невозможно предсказать, какое имя импортировать. Это один из компромиссов при работе с фреймворками. В данном случае компромисс, по-видимому, того стоит. Выборка данных Отлично! У нас 2 результата из 3. У нас есть серверный рендеринг и маршрутиза­ ция на основе файловой системы, но мы по-прежнему страдаем от "сетевых водо­ падов". Давайте исправим метод получения данных. Для начала мы обновим наши компоненты, чтобы они могли получать исходные данные через пропсы. Для про­ стоты мы рассмотрим только компонент списка Ltst, а компонент Detat l вам в качестве домашней работы: // ./pages/list.jsx // ЗамеР1ьР1е, ЧРIО экcnof)PI для // происходиРI по умолчанию. мaptJJpyl'luзaцuu, основанной на файловой cucf'/eмe, export defautt functi.on Ltst({ tntttalThtngs} //<-добавляем /* изначальный пропс { const [thtngs, setThtngs] = useState(tntttalThtngs); const [requestState, setRequestState] = useState("tntttal"); const [еггог, setError] = useState(nutt); // Moжef'I рабоР1аР1ь для получения данных, useEffect(() => { tf (tntttalThtngs) return; setRequestState("loadtng"); fetch("https://apt.cor,/get-Hst") если нам Эf'IO Рlребуеf'/Ся. */) оставим
Фреймворки 1 267 .then((r) => r.json()) .then(setТhi.ngs) . then( () => { setRequestState("success"); }) .catch(( е) => { setRequestState("error"); sеtЕггог(е); }); }, [i.ni.ti.alThi.ngs]); return ( <di.v> <Ul> {thi.ngs.мap((thi.ng) <li. => ( key={thi.ng.i.d}>{thi.ng.laЬel}</lt> ))} </ul> </dtv> ); } Отлично. Теперь, когда мы добавили начальный пропс, нам нужен какой-то способ получить данные, необходимые для этой страницы, на сервере, а затем передать их компоненту перед рендерингом. Давайте посмотрим, как можно это сделать. В идеале мы хотим сделать следующее: // ./seгveг.js i.Plport express frOl'I "express"; i.Plport { joi.n} frOl'I "path"; i.Plport { renderToStri.ng} frOl'I const арр "react-doм/server"; // Описано в главе б = express(); app.use(express.stati.c("./di.st")); // // const createLayout = (chi.ldren) => <head> <ti.tle>My page</ti.tle> </head> Получим сf'/аf'lические файлы, f'laкue как '<htмl client JS lang="en">
268 Глава 8 <Ьоdу> ${chi.ldren} <scri.pt src="/cli.entBundle.js"></scri.pt> </Ьоdу> <htl"ll>'; app.get("/:route", async (геq, геs) => { const exportedStuff = awai.t 1J11port( joi.n(process.cwd(), "pages", req.paral"ls.route) ); const Page = exportedStuff.default; // Получим данные компонен~а const data = awai.t exportedStuff.getData(); const ргорs = req.query; res.setl-leader("Content-Type", "text/htl"ll"); // Передадим пропс и данные res.end(createlayout(renderToStri.ng(<Page { ... ргорs} { ... data.props} />))); }); app.li.sten(3000, () => { console.i.nfo("App i.s li.steni.ng!"); }); Это означает, что нам нужно будет экспортировать функцию выборки, называемую getData, из любых компонентов страницы, которым нужны данные! Давайте скор­ ректируем список, чтобы сделать это: // ./poges/list.jsx // Ми вызываем на сервере и передаем э~и пропсы компонен~у. const getData = async () => { return { ргорs: { i.ni.ti.alТhi.ngs: awai.t fetch("https://api...cOl"l/get-li.st").then((r) => г .json() ), ехрогt }, }; };
Фреймворки ехрогt default functi.on Li.st({ i.ni.ti.alThi.ngs} 1 269 /*<-добавляем*/ /* изначальный пропс */) { const [thi.ngs, setThi.ngs] = useState(i.ni.ti.alThi.ngs); const [гequestState, setRequestState] = useState("i.ni.ti.al"); const [еггог, sеtЕггог] = useState(null); // Можеf'I робОf'/йl'lь для получения данных, если нам Эf'/0 f'lребуеР1Ся. useEffect(() => { tf (i.ni.ti.alThi.ngs) гetuгn; setRequestState("loadi.ng"); getData() .then(setТhi.ngs) . then( О => { setRequestState("success"); }) .catch((e) => { setRequestState("erroг"); sеtЕггог(е); }); }, [i.ni.ti.alThi.ngs]); гetuгn ( <di.v> <Ul> {thi.ngs.l'lap((thi.ng) => ( <li. key={thi.ng.i.d}>{thi.ng.laЬel}</lt> ))} </Ul> </dtv> ); } Готово! Теперь мы: ♦ загружаем данные на сервере как можно раньше для каждого маршрута в каждом файле; ♦ отображаем полную страницу в виде НТМL-строки; ♦ отправляем это клиенту. Мы успешно добавили и разобрались с тремя функциями, которые мы выявили в различных фреймворках, и внедрили их базовую версию. Сделав это, мы узнали и теперь понимаем основной механизм работы фреймворков.
270 Глава 8 В частности, мы увидели: ♦ как фреймворки обеспечивают рендеринг на сервере; ♦ что они имеют изоморфную маршрутизацию, зависящую от файловой системы; ♦ как фреймворки извлекают данные с помощью экспортируемых функций. Если вы использовали Next.js версии ранее 13, то на этом этапе должно стать со­ вершенно ясно, почему в нем применяются различные шаблоны, в частности, шаб­ лоны, касающиеся: ♦ каталога ♦ экспорта страниц по умолчанию; ♦ getSeгveгSi.deProps и getStati.cProps. ./pages; Теперь, когда мы понимаем механизм, лежащий в основе фреймворков, на уровне кода, а также причины некоторых из их соглашений, давайте подытожим преиму­ щества использования фреймворка. Преимущества использования фреймворка К преимуществам использования фреймворка относятся следующие аспекты. Структура и согласованность. Фреймворки предоставляют определенную структуру и шаблон для организации кодовой базы. Это обеспечивает согласованность, облегчая новым разработчи­ кам понимание работы приложения. Поэтому мы можем сосредоточиться на наших продуктах и функциях, не беспокоясь о том, как структурировать наш код. Лучшие практики. Фреймворки часто содержат готовые рекомендации, которым могут следовать разработчики. Это ведет к повышению качества кода и уменьшению количества ошибок. Например, фреймворки могут побуждать вас получать данные на ран­ нем этапе, т. е. на сервере, а не ждать, пока их получит клиент. Это способствует повышению производительности приложения и улучшению работы пользователя. Высокий уровень абстракции. Фреймворки предоставляют абстракции более высокого уровня для решения та­ ких распространенных задач, как маршрутизация, выборка данных, рендеринг на сервере и многое другое. В результате ваш код может стать более чистым, удобочитаемым и простым в обслуживании, а также обеспечить качество этих абстракций, опираясь на более широкое сообщество. Примером этого может служить функция useRouteг, предоставляемая Next.js, которая упрощает доступ к маршрутизатору в ваших компонентах. Оптимизация производительности. Многие фреймворки поставляются с готовыми оптимизациями, такими как раз­ деление кода, рендеринг на стороне сервера и создание статического сайта. Это
Фреймворки 271 может значительно повысить производительность вашего приложения. Напри­ мер, Next.js автоматически фрагментирует код и предварительно загружает код для следующей страницы, когда пользователь наводит курсор мыши на ссылку, что ускоряет переходы по страницам. Сообщество и экосистема. Популярные фреймворки имеют большое сообщество и богатую экосистему подключаемых модулей и библиотек. Это означает, что вы часто можете быстро найти решение или обратиться за помощью, если столкнетесь с проблемой. Компромиссы использования фреймворка Хотя фреймворки обладают многими преимуществами, они не лишены недостат­ ков. Понимание этого может помочь вам принять обоснованное решение о том, стоит ли применять фреймворк и какой из них выбрать. Процесс освоения. Каждый фреймворк поставляется со своим собственным набором концепций, API и соглашений, которые вам необходимо изучить. Если вы новичок в одновременное изучение React чей, но этот метод заслуживает рекомендации. Если вы уже знакомы с вам нужно потратить время React, и фреймворка может оказаться непростой зада­ на изучение специфических функций и React, АРI­ интерфейсов этого фреймворка. Гибкость против соглашений. Хотя принудительная структура и соглашения фреймворка могут быть благом, они также могут вносить и ограничения. Если ваше приложение имеет уникаль­ ные требования, которые не вписываются в модель фреймворка, вы можете столкнуться с тем, что будете бороться с фреймворком, а не получать от него помощь. В некоторых случаях, если вы создаете приложение для определенной группы пользователей с быстрым доступом в Интернет и современными браузе­ рами, вам может не понадобиться рендеринг на стороне сервера или серверная выборка данных. В таких случаях использование фреймворка может оказаться излишним. Зависu./lюсть и обязательства. Выбор платформы - это своего рода решимость. Вы связываете свое приложе­ ние с судьбой платформы. Если фреймворк перестанет поддерживаться или если он примет направление, не соответствующее вашим потребностям, вы можете столкнуться с трудностями при принятии решения о том, следует ли осущест­ вить дорогостоящий переход на другой фреймворк или самостоятельно поддер­ живать существующий код фреймворка. Накладные расходы на абстрагирование. В то время как абстракции могут упростить разработку, нивелируя ее слож­ ность, они также могут создавать "магию", которая затрудняет понимание того,
Глава 272 8 что происходит "под капотом". Это может усложнить отладку и настройку про­ изводительности. Кроме того, каждая абстракция сопряжена с некоторыми на­ кладными расходами, которые могут повлиять на производительность. Приме­ ром этого являются действия сервера в Next.js, где директива "use sегvег" каким­ то волшебным образом запускает некоторое действие на сервере. Это отличная абстракция, но иногда бывает трудно понять, как она работает. Теперь, когда мы понимаем, почему нам может понадобиться фреймворк также осознаем связанные с ним преимущества и компромиссы, биться в изучение конкретных фреймворков в экосистеме React. мы React, а можем углу­ В следующих раз­ делах этой главы мы рассмотрим некоторые популярные варианты, такие как Next.js и Remix. Каждый фреймворк обладает уникальными особенностями и пре­ имуществами, и понимание их принципов позволит вам выбрать правильный инст­ румент для ваших конкретных нужд. Популярные фреймворки React Давайте рассмотрим некоторые из популярных фреймворков React и обсудим их особенности, преимущества и компромиссы. Мы начнем с краткого обзора каждого фреймворка, за которым последует подробное сравнение их возможностей и произ­ водительности. Мы также обсудим некоторые факторы, которые следует учитывать при выборе фреймворка для вашего проекта. Remix Remix - это мощный современный фреймворк, который использует React и воз­ можности веб-платформы. Давайте начнем с нескольких практических примеров, чтобы понять, как это работает. Базовое приложение Remix Сначала мы настроим базовое приложение Remix. Вы можете установить Remix с npl'l: nl)l'l сгеаtе reP1tx@2.2.0 помощью Эта команда создаст новый проект Remix в вашем текущем каталоге. Давайте оста­ новимся, чтобы посмотреть, что находится внутри. Начнем с того, что у нас есть каталог арр с файлами сутствует файл root.tsx и entry.server.tsx. В этом каталоге также при­ root.tsx. Сразу видно, что того, entry.client.tsx Remix поддерживает клиентскую и серверную точки входа. Более содержит компонент общего макета, который отображается на каждой странице. Это отличный пример того, как Remix предоставляет предопределенную структуру, которая поможет вам быстро приступить к работе.
Фреймворки \ 273 server.tsx. Этот Серверный рендеринг Remix обеспечивает серверный рендеринг "из коробки" с помощью файл сгенерирован для нас, но давайте немного разберемся в нем. Вот как он вы­ глядит: 'U'lpoгt { PassThrough} 'U'lpoгt type { ApploadContext, EntryContext} fгOl'I "@rel'lt.x-гun/node"; { createReadaЫeStreaP1Fr~eadaЫe} fгOl'I "@reP1t.x-run/node"; { ReP1t.xServer} fгOl'I "@rel'lt.x-гun/react"; Vlpoгt Vlpoгt fгOl'I "node:stгeaPl"; Vlpoгt t.sЬot fгOl'I "t.sЬot"; { V1poгt const renderToPt.peaЫeStreal'l} fгOl'I AВORT_DELAY "react-dOP1/server"; = 5_000; default functi.on handleRequest( request: Request, ехрогt гesponseStatusCode: nul'lЬer, responseHeaders: Headeгs, reP1t.xContext: EntгyContext, loadContext: ApploadContext ) { гetuгn t.sbot(request.headeгs.get("useг-agent")) ? handleBotRequest( request, responseStatusCode, responseHeaders, reP1t.xContext ) handleBгowserRequest( request, responseStatusCode, гesponseНeadeгs, rel'lt.xContext ); } function handleBotRequest( request: Request, responseStatusCode: nuP1Ьer, responseHeadeгs: Headeгs, reP1t.xContext: EntryContext
Глава 274 8 ) { return new Proмtse((resolve, reject) => { let shellRendered = fatse; const { ptpe, аЬогt} = геndегТоРtреаЫеStгеам( <Reflli.xServer context={reмtxContext} url={request.url} aЬortDelay={AВORT_DELAY} />, { onAllReady() { shellRendered = true; const body = new PassThгough(); const stгеам = createReadaЫeStreaмFroмReadaЫe(body); responseHeaders.set("Content-Type", "text/htмl"); resolve( new Response(streaм, { headers: responseHeadeгs, status: responseStatusCode, }) ); ptpe(body); }, onShellError(error: unknown) { геjесt(еггог); }, unknown) { responseStatusCode = 500; // РегuсР1рuруйР1е 0/l/uбки nоР1окового рендеринга внуРlри оболочки. // Не регuсР1рuруйР1е 0/l/uбки, возникшие при первоначальном рендеринге // оболочки, поскольку они будуРI обрабоР1аны в handleDocuP1entRequest. tf (shellRendered) { оnЕггог(еггог: соnsоlе.еггог(еггог); } }, } );
Фреймворки setТl111eout(aЬort, AВORT_DELAY); }); } functi.on handleBrowserRequest( request: Request, responseStatusCode: nu~Ьег, responseНeaders: Неаdегs, r~txContext: EntгyContext ) { return new Pr~tse((resolve, reject) => { tet shellRendered = fatse; const { ptpe, аЬогt} = renderToPtpeaЫeStrea~( <Rerтt.xServer context={re~txContext} url={request.url} aЬortDelay={AВORT_DELAY} />, { onShellReady() { shellRendered = true; const Ьоdу = new PassThrough(); const strea~ = createReadaЫeStrea~Fr~eadaЫe(Ьody); responseНeaders.set("Content-Type", resolve( new Response(strea~, { headers: responseHeaders, status: responseStatusCode, }) ); ptpe(Ьody); }, onShellError(error: unkn~n) { геjесt(еггог); }, unknown) { responseStatusCode = 500; оnЕггог(еггог: "text/ht~l"); 1 275
Глава 276 8 // Регисмрируйме О11Jибки nОf'lокового рендеринга внумри оболочки. // Не регисмрируйме 0//Juбки, возникшие при первоначальном рендеринге // оболочки, поскольку они будум обрабОf'lаны в handleDocuмentRequest. tf (she11Rendered) { console.error(error); } }, } ); setTil'leout(aЬort, AВORT_DELAY); }); } Самое замечательное в Remix то, что этот файл используется внутри системы, но он доступен для нас здесь для настройки. Если мы удалим этот файл, Remix перей­ дет к своей внутренней реализации того же файла по умолчанию. Это хороший за­ пасной выход, который позволяет нам настраивать поведение серверного ренде­ ринга, если это необходимо, но при этом не ограничивает нас, что называется, "магией" фреймворка. Этот файл определяет, как должны генерироваться и обрабатываться НТТР-ответы в нашем приложении Remix, особенно в отношении того, как по-разному управля­ ются запросы от ботов и обычных браузеров. современных приложений приложения React, Remix - это фреймворк для создания и этот файл является частью серверной логики Remix. Изначально файл импортирует необходимые модули и типы из различных библио­ тек, таких как node:streal'l, @rel'lix-run/node, @rel'lix-run/react, isЬot и react-dOl'l/server. Он определяет константу AВORT_DELAY со значением 5000 миллисекунд, которая ис­ пользуется в качестве периода ожидания для операций рендеринга. Файл экспортирует функцию handleRequest по умолчанию, которая принимает не­ сколько аргументов, включая НТТР-запрос, код статуса ответа, заголовки ответа, контексты для Remix и процесс загрузки приложения. Внутри handleRequest он про­ веряется useragent входящего запроса, чтобы определить, исходит ли он от бота, использующего библиотеку isЬot. В зависимости от того, поступает ли запрос от бота или браузера, производится обработка либо функцией handleBotRequest, либо функцией handleBrowserRequest соответственно. Это улучшает SEO и производительность. Например, если запрос поступает от бо­ та, важно убедиться, что ответ содержит отображаемый НТМL-контент страницы, что и делает handleBotRequest. С другой стороны, если запрос поступает из обычного браузера, важно удостовериться, что ответ содержит отображаемый НТМL-контент страницы вместе с необходимым JavaScript-кoдoм для гидратации страницы, что и
Фреймворки производит handleBrowserRequest. Очень здорово, что Remix 277 автоматически справля­ ется с этим за нас. Функции handleBotRequest и handleBrowserRequest довольно схожи по структуре, но имеют разные обработчики для случаев, когда отображаемая оболочка готова или обнаруживается ошибка. Они возвращают обещание (promise ), которое преобразу­ ется в НТТР-ответ. Они инициируют операцию рендеринга в конвейерном потоке с помощью renderT0Pi.peaЫeStrearr1, передавая компонент Re111i.xServer вместе с необхо­ димым контекстом и URL-aдpecoм из запроса. Они определяют время ожидания, чтобы прервать операцию рендеринга, если она занимает больше времени, чем зна­ чение AВORT_DELAY. В обработчиках событий для операции рендеринга создается поток PassThrough, а из него - поток для чтения. Для ответа они устанавливают заголовок Content- Туре в значение text/ht111l. Они разрешают обещание (promise) с помощью нового объекта Response, который инкапсулирует поток, заголовки ответов и код состояния. В слу­ чае ошибок во время рендеринга они либо отклоняют обещание гистрируют ошибку в консоли - (promise), либо ре­ в зависимости от стадии рендеринга, на которой произошла ошибка. Этот файл, по сути, гарантирует, что НТТР-ответы будут правильно сгенерированы и возвращены с применением различной логики рендеринга в зависимости от того, поступает ли запрос от бота или от обычного браузера. Это имеет решающее зна­ чение для SEO и повышения производительности в современных веб-приложениях. Если нам не нужно ничего настраивать, мы можем просто удалить этот файл, и Remix обработает серверный рендеринг за нас. Давайте пока оставим его и посмот­ рим, как Remix обрабатывает маршруты. Маршрутизация В Remix каждый маршрут представлен файлом в каталоге routes. Если .lroutes/cheese.tsx, экспорт которого по умолчанию выглядит как: ехрогt default function CheesePage() { мы создадим файл гetuгn <h1>Thi.s 1111.ght sound cheesy, but I thi.nk уоu'ге really grate!</h1>; } то затем, запустив локальный сервер разработки с помощью np111 run dev, мы увидим страницу с забавным заголовком'. В очередной раз мы видим, что Remix предос­ тавляет предопределенную структуру, которая поможет вам быстро приступить к работе, а значение экспорта по умолчанию в этом соглашении аналогично нашей собственной реализации маршрутизации на основе представленной ранее файловой системы. В сочетании с компонентом общего макета в ./app/root.tsx и точками вхо­ да для сервера и клиента это составляет основу большинства веб-сайтов. Однако might sound cheesy, but I think you're really grate. - (дословно) Это может показаться баналь­ (grated (cheese)- тертый (сыр); тертый калач, возможно, более по-русски). Grate созвучно great (великолепна). - Прим. пер. 1 This ным, но я думаю, что ты действительно тертый сыр
278 Глава 8 нам по-прежнему не хватает одного важного компонента для современного веб­ приложения: выборки данных. Давайте посмотрим, как Remix справляется с этим. Получение данных История получения данных Remix на момент написания книги включает в себя ис­ пользование функций, называемых загрузчиками. Когда вы экспортируете асин­ хронную функцию loader, которая возвращает некоторое значение, это значение становится доступным для вашего компонента страницы через хук useLoaderData. Давайте посмотрим, как это работает, на примере. Для того чтобы вернуться к нашей сырной странице хотим получить список сыров из API CheesePage, предположим, мы и отобразить его на странице. Мы можем сде­ лать это, экспортировав функцию загрузки из ./routes/cheese.tsx: / / Получим YPIUЛUPIЫ t,,,port { json} frOl'I "@re111i.x-run/node"; t,,,port { useLoaderData} frOl'I "@re111i.x-run/react"; // Загрузчик export async functi.on loader() { const data = await fetch("https://api..co111/get-cheeses"); return json(await data.json()); } export default functi.on CheesePage() { const cheeses = useLoaderData(); return ( <d\.V> <h1>Thi.s 111i.ght sound cheesy, but I thi.nk you're really grate!</h1> <Ul> {cheeses.111ap((cheese) => ( <li key={cheese.i.d}>{cheese.na111e}</li> ))} </ul> </div> ); } Таким образом, мы видим повторение нашей предыдущей реализации извлечения данных. Мы можем видеть, что Rеmiх-функция ную функцию getData. loader похожа на нашу собствен­ Мы также можем увидеть, насколько механизм похож на наш собственный про пс i.ni. ti.a l Thi.ngs. useLoaderData В идеале на данный момент мы можем выявить общие закономерности и механизмы, лежащие в основе того, как фреймворки реализуют эти функции.
Фреймворки 1 279 До сих пор мы рассматривали: ♦ серверный рендеринг; ♦ маршрутизацию; ♦ выборку данных. Но есть еще одна особенность Remix, которую мы пока не анализировали: формы и действия сервера, или мутации, т. е. изменение данных на сервере, например: соз­ дание, обновление или удаление данных. Давайте рассмотрим это далее. Мутация данных Remix был ответственен за возвращение Интернета к его основам, в значительной степени опираясь на нативные соглашения о поведении веб-платформы. Это лучше всего видно на примере мутации данных и использования Remix-фopм. Давайте расширим наш предыдущий сырный пример: сделаем список сыров изменяемым. Для этого начнем с обновления нашего файла // ./routes/cheese. tsx: Получим Yf'IUЛUl'IЬI 'U'lport { json} frOl'I "@гer,ix-run/node"; 'U'lport { useLoaderData} frOl'I "@re111ix-run/react"; // Загрузчик export async functi.on loader() { const data = awai.t fetch("https://api.cм/get-cheeses"); return json(awai.t data.json()); } export defau\t functi.on CheesePage() { const cheeses = useLoaderData(); return ( <di.v> <h1>This 111ight sound cheesy, but I think уоu'ге really grate!</h1> <U\> {cheeses.111ap((cheese) => ( <1'\. key={cheese.id}>{cheese.na111e}</1i.> ))} </u1> <fom action="/cheese" 111ethod="post"> <i.nput type="text" na111e="cheese" /> <Ьutton type="suЬl'lit">Add Cheese</Ьutton> </fom> </di.V> ); }
280 Глава 8 Обратите внимание, что мы добавили на страницу новый элемент forl"I. В этой фор­ /cheese и метод post. Это стандартная НТМL-форма, которая от­ правляет запрос POST по маршруту /cheese. Более того, у поля ввода i.nput есть ат­ рибут nal"le и нет обработчика useState или onChange: Remix позволяет браузеру ме есть действие самому управлять состоянием и поведением формы. Это хороший пример того, как Remix опирается на веб-платформу, чтобы обеспечить отличный опыт работы с разработчиками, и не пытается изобретать велосипед, заставляя React управлять абсолютно всем. Учитывая, что свойство действия формы cheese.tsx каталога ./routes/, - /cheese, а мы уже находимся в файле мы можем предположить, что форма будет отправлена по тому же маршруту. Когда доступ к этому маршруту осуществляется с помощью метода POST, мы знаем, что форма отправлена. Когда по умолчанию осуществляется доступ к этому маршруту с помощью метода GЕТ, мы знаем, что форма не была от­ правлена, и вместо этого показываем исходный пользовательский интерфейс. Давайте обновим наш файл // справиться с этим: Получим Yf'IUЛUfТIЬI 1.l'lpoгt 1.l'lpoгt // .lroutes/cheese.tsx, чтобы { json, Acti.onFuncti.onArgs, redi.rect} fгOl"I { useloadeгData} fгOl"I "@rel"li.x-гun/гeact"; "@rel"li.x-гun/node"; Загрузчик async function loader() { const data = awai.t fetch("https://api..cOl"l/get-cheeses"); гetuгn json(awai.t data.json()); ехрогt } / / ДeЙCf'IBUe фopt,,t,I async function acti.on({ paral"ls, request }: Acti.onFuncti.onArgs) { const forl"IData = awai.t request.forl"IData(); ехрогt awai.t fetch("https://api..cOl"l/add-cheese", { l"lethod: "POST", body: JSON.stri.ngi.fy({ nal"le: forl"IData.get("cheese"), }), }); гetuгn redi.rect("/cheese"); // Вернемся на сf'lраницу, но уже с } default function CheesePage() { const cheeses = useLoaderData(); ехрогt GET
Фреймворки return ( <di.v> <h1>Thi.s <Ul> мi.ght sound cheesy, but I thi.nk you're really grate!</h1> {cheeses.мap((cheese) <li. 281 => ( key={cheese.i.d}>{cheese.naмe}</li.> ))} </ul> <foП'I acHon="/cheese" <i.nput type="text" мethod="post"> naмe="cheese" /> <Ьutton type="suЬмi.t">Add Cheese</Ьutton> </forм> </di.v> ); } acti.on, которая принимает request. Аргумент рагамs - это объект, содержащий параметры Аргумент request- это объект, содержащий объект запроса. Мы можем Обратите внимание, что мы добавили новую функцию аргументы рагамs и маршрута. использовать их, чтобы получить данные формы из запроса, а затем применить их для отправки запроса в наш API для добавления в список нового сыра. Затем мы возвращаем редирект на тот же маршрут, но на этот раз с помощью мето­ да GЕТ. Это приведет к перезагрузке страницы, и функция загрузки loader будет вы­ звана снова, чтобы получить обновленный список сыров. Таким образом, вать JavaScript Remix полностью опирается на веб-платформу, позволяя использо­ там, где это необходимо, и разрешая браузеру обрабатывать осталь­ ное. Если бы эта страница была отрисована без JavaScript, она бы работала только потому, что она опирается на веб-платформу. Если страница использует Remix JavaScript, постепенно улучшает взаимодействие с ней, добавляя интерактивности и улучшая взаимодействие с пользователем. До сих пор мы рассказывали о том, как ♦ обеспечивает серверный рендеринг; ♦ управляет маршрутизацией; ♦ управляет выборкой данных; ♦ управляет изменениями данных. Remix: На текущий момент мы должны видеть явные параллели между нашей собственной имплементацией этих функций и реализацией Remix. Это отличный признак, ука­ зывающий, что мы понимаем механизмы, лежащие в основе того, как фреймворки реализуют эти функции.
Глава 282 8 Теперь давайте рассмотрим Next.js и выясним, как он выполняет очень похожие действия, а затем выявим общие черты, лежащие в основе способов реализации этих функций. Next.js Next.js, популярный фреймворк React от компании Vercel, хорошо известен своими богатыми возможностями и простотой создания статических веб-сайтов и веб­ сайтов с рендерингом на стороне сервера (server-side rendered, SSR). Он следует принципу "соглашение важнее конфигурации", что сокращает количество шабло­ нов и процедур принятия решений, необходимых для запуска проекта. С выпуском Next.js версии 13 важным дополнением стало внедрение Давайте пройдемся по основам Next.js, Next.js Арр Router. чтобы понять, как он работает. Для начала запустим следующую команду, чтобы создать новый проект Next.js: np~ create next-app@14 Это вызовет несколько вопросов, но в конечном итоге мы придем к основам Next.js. Давайте осмотримся и попробуем разглядеть, что там внутри. Начнем с того, что у нас есть каталог приложений арр с файлами page. tsx, Одна вещь, которую мы сразу замечаем, это то, что рацию сервера, как это делает Remix, /ayout. tsx, error. tsx и /oading. tsx. Next.js не раскрывает конфигу­ а вместо этого прячет большое количество сложностей с намерением "не мешаться" и позволить разработчикам сосредото­ читься на создании своего приложения. Это отличный пример того, как различные фреймворки предлагают разную философию и подходы к решению одних и тех же проблем. Давайте рассмотрим Next.js в контексте трех ключевых функций, которые мы оп­ ределили ранее: серверного рендеринга, маршрутизации и выборки данных. Серверный рендеринг Next.js не только обеспечивает рендеринг на стороне сервере, но делает это в пер­ вую очередь. Каждая страница и компонент в Next.js являются серверным компо­ нентом. Мы довольно подробно рассмотрим серверные компоненты в главе пока достаточно сказать, что серверные компоненты - 9, но это компоненты, отобра­ жаемые исключительно на сервере. На данный момент этот уровень понимания достаточен, поскольку основное внимание уделяется Next.js, а не серверным ком­ понентам. Для серверных компонентов должно быть достаточно материала, изло­ женного в главе 9. Что это означает в контексте Next.js? По сути, мы должны оперировать исходя из того, что весь код, который мы пишем, выполняется исключительно на сервере, если не указано иное путем добавления директивы "use cli.ent" ("использовать кли­ ент") в начало маршрута или компонента. Без этой директивы весь код считается сер верным. Однако Next.js также сначала выполняет статическую обработку: во время сборки серверные компоненты отображаются с максимально возможным статическим со-
Фреймворки держимым, а затем развертываются. Такое сочетание Next.js server-first и \ 283 static-first делает таким мощным, а это значительно повышает производительность. Статиче­ ский контент, возможно, быстрее всего доходит до пользователей, поскольку для этого не требуется обработка на стороне сервера - это просто текст (HTML). Сле­ дующим шагом после статического является отображаемый сервером контент, ко­ торый может быть оптимизирован и кеширован, но для отображения контента по­ прежнему требуется сервер. Завершающим этапом является создание отображаемо­ го клиентского контента с помощью гидратации интерактивных частей страницы. При таком подходе пакеты JavaScript, пользователям могут быть предоставлены небольшие Next.js при этом основная часть контента представляет собой смесь ста­ тической разметки и разметки, отображаемой сервером. Уровень детализации, с которым могут отображаться на сервере не только страницы, но и компоненты, это сильная сторона Next.js, - позволяющая использовать некоторые очень мощные шаблоны выборки и рендеринга данных. Прежде чем мы перейдем к этим шабло­ нам, давайте посмотрим, как Next.js управляет маршрутизацией. Маршрутизация Что мы видим в нашем новом проекте файлами layout.tsx и page.tsx. Next.js так это каталог приложения арр с Next.js, следует такому шаблону: путь, который ваши пользователи видят в своем браузере, URL-aдpec вашей страницы, является именем каталога, в котором каталог арр приложения эквивалентен корневому каталогу (/), и каждый каталог под ним становится подпутем. Для того чтобы понять это, давайте создадим каталог cheese и добавим в него файл page.tsx, этот каталог становится маршру­ том. Давайте добавим некоторое содержимое в ./app/cheese/page.tsx: ехрогt defautt functi.on CheesePage() { гetuгn <h1>This мight sound cheesy, but I think you're really grate!</h1>; page.tsx. Когда в каталоге ./арр есть файл } Теперь, если мы запустим сервер разработки и перейдем в /cheese, мы увидим нашу Next.js также сущест­ страницу с забавным заголовком. Здесь стоит отметить, что в вует концепция общих макетов, аналогичная Remix, где вы можете определить компонент макета в ./app/layout.tsx, и он будет отображаться на каждой странице. ./app/cheese/layout.tsx будет отображаться на каждой странице в маршруте /cheese. Макеты обычно представляют собой части маршрутов, которые совместно Затем используются на нескольких страницах, например верхний или нижний колонтитул или другие фиксированные элементы. Отлично, теперь понятно, как ром роде похоже на Remix Next.js обрабатывает маршрутизацию. Это в некото­ и нашу собственную реализацию маршрутизации на ос­ нове файловой системы, но с небольшим отличием в том, что страницей становится не отдельный файл, а целый каталог, и ожидается, что фактическая страница все время будет называться page.tsx. Кроме этого, все остальное довольно похоже. Давайте поговорим о выборке данных.
Глава 284 8 Выборка данных Поскольку компоненты в Next.js - серверные, они асинхронные и, следовательно, ожидают получения данных. Давайте попробуем получить список сыров, как мы делали в нашем предыдущем примере в Remix, но на этот раз в Next.js: defautt async functi.on CheesePage() { const cheeses = awa"i.t fetch("https://apt.co111/get-cheeses").then((r) => г .json() ехрогt ); return ( <d"i.v> <h1>This 1111.ght sound cheesy, but I think you're really grate!</h1> <Ut> {cheeses.111ap((cheese) => ( <t"i. key={cheese.td}>{cheese.narчe}</t"i.> ))} </Ut> </d"i.v> ); } Именно на такой синтаксис многие годы полагались инженеры естественно. Это возможно, потому что CheesePage React, и это вполне здесь является серверным ком­ понентом: он не входит в клиентский пакет и вместо этого отображается на серве­ ре. Это означает, что мы можем ожидать данные и асинхронно отображать их непо­ средственно на странице. Поскольку все компоненты являются серверными, мы можем еще больше повысить степень детализации и выполнять выборку не на уровне страницы, а на уровне компонента, если захотим. Представьте разбивку этой страницы на более мелкие компоненты, где список сыров CheeseL ist можно использовать повторно, как он ис­ пользуется на этой странице, но может быть выведен и в других местах. Наша страница станет такой: 'U'lpoгt { Cheeselist} frOl'1 "./Cheeseltst"; defautt functi.on CheesePage() { return ( <d"i.v> <h1>This 1111.ght sound cheesy, but I thtnk you're really grate!</h1> <CheeseL"i.st /> </d"i.V> ехрогt ); }
Фреймворки 1 285 и наш компонент Cheeseli.st примет следующую форму: ехрогt async functi.on Cheeseli.st() { const cheeses = awai.t fetch("https://api..cOl'l/get-cheeses").then((r) => г .json() ); return ( <Ul> {cheeses.~ap((cheese) => ( <li. key={cheese.i.d}>{cheese.na~}</li.> ))} </Ul> ); } Истинная сила этого подхода заключается в том, что мы можем извлекать данные на уровне компонентов, а затем отображать их на странице. Мы не экспортируем функции с getStati.cProps уровня страницы, такие как loader, getData, getServerSi.deProps, или что-либо подобное. Вместо этого мы просто извлекаем данные на уровне компонента и отображаем их на странице. Что происходит с этими данными? Next.js использует их для статического создания первой загрузки нашей страницы при развертывании, а сервер отображает их при последующих загрузках. Next.js также имеет ряд механизмов кеширования и деду­ пликации, которые обеспечивают целостность данных и производительность. Наконец, давайте завершим сравнение, изучив, как в Next.js обрабатываются мута­ ции данных. Мутация данных Next.js использует концепцию серверных действий (server actions), которые пред­ ставляют собой функции, выполняемые на сервере. Это функции, которые вызы­ ваются при отправке формы, нажатии пользователем кнопки или переходе на дру­ гую страницу. Это функции, которые выполняются на сервере и не включены в клиентский пакет. Давайте рассмотрим, как добавить сыр в список, как мы это делали в нашем приме­ ре с Remix. Для этого в нашу страницу мы { Cheeseli.st} frOl'I "./Cheeseli.st"; ~рогt ~рогt ~гt { redi.rect} frOl'I "next/navi.gati.on"; { revali.datePath} frOl'I "next/cache"; export default functi.on CheesePage() { return ( вставим НТМL-форму:
Глава 286 8 <di.v> <h1>Thts мtght sound cheesy, but I thtnk you're really grate!</h1> <CheeseLi.st /> <fогм actton={async (forмData) => { "use server"; awai.t fetch("https://apt.coм/add-cheese", { мethod: "POST", body: JSON.strtngtfy({ nаме: forмData.get("cheese"), }), }); revaltdatePath("/cheese"); return redtrect("/cheese"); }} мethod="post" > <i.nput type="text" naмe="cheese" /> <button type="subмtt">Add Cheese</button> </forм> </di.v> ); } Здесь мы используем стандартную НТМL-форму, похожую на употребленную на­ ми в Remix, за исключением того, что атрибут actton на этот раз является функци­ ей. Эта функция является серверным действием и вызывается при отправке формы. Функция не включена в клиентский пакет и вместо этого запускается на сервере. Это обеспечивается директивой "use server". Мы могли бы переместить эту функцию куда угодно, в том числе в тело серверного компонента, например, так: V!port { redtrect} fгом "next/navtgatton"; V1port { revaltdatePath} fгом "next/cache"; export default functi.on CheesePage() { async functi.on addCheese(forмData) { "use server"; awai.t fetch("https://apt.coм/add-cheese", { мethod: "POST", body: JSON.strtngtfy({
Фреймворки 1 287 nаме: forrтiData.get("cheese"), }), }); revali.datePath("/cheese"); return redi.rect("/cheese"); } return ( <di.v> <h1>Thi.s rтii.ght sound cheesy, but I thi.nk you're really grate!</h1> <CheeseLi.st /> <foГl'I acti.on={addCheese} мethod="post"> <i.nput type="text" naмe="cheese" /> <button type="subrтii.t">Add Cheese</button> </fOГl"t> </di.v> ); } или даже в отдельный модуль, например, так: i.l"lport { addCheeseActi.on} frOPI "./addCheeseActi.on"; default functi.on CheesePage() { return ( <di.v> <h1>Thi.s rтii.ght sound cheesy, but I thi.nk you're really grate!</h1> <CheeseLi.st /> <forl"t acti.on={addCheese} rтiethod="post"> <i.nput type="text" naмe="cheese" /> <button type="subrтii.t">Add Cheese</button> ехрогt </fOГl"t> </di.v> ); } В этом случае действие addCheeseActi.on будет находиться в отдельном файле и мо­ жет выглядеть следующим образом: "use server"; { redi.rect} frOP1 "next/navi.gati.on"; tl"tport { revali.datePath} frOPI "next/cache"; tl"tpoгt
Глава 288 8 async functi.on addCheeseActi.on(forмData) { awai.t fetch("https://api..coм/add-cheese", { мethod: "POST", Ьоdу: JSON.stri.ngi.fy({ ехрогt nаме: forмData.get("cheese"), }), }); revali.datePath("/cheese"); гetuгn redi.rect("/cheese"); } Однако здесь есть внутренняя проблема. В отличие от Remix, где все компоненты являются клиентскими, серверные компоненты вообще не поддерживают интерак­ тивность, поскольку они не включены в клиентский пакет и никогда не загружают­ ся браузером; таким образом, обработчики onCl i.ck на самом деле вообще не обраба­ тывают клики пользователей. Для того чтобы решить эту проблему, в Next.js существует концепция клиентских компонентов, которые включены в клиентский пакет и загружаются браузером. Эти компоненты не являются серверными компо­ нентами и, следовательно, не могут быть асинхронными или выполнять какие-либо серверные действия. Давайте рассмотрим возможность добавления сыра, но на этот раз с использовани­ ем сочетания серверного и клиентского компонентов. Это также поможет нам реа­ гировать на отправку формы, немедленно предоставляя обратную связь с помощью спиннера или чего-то подобного. Для этого мы создадим новый компонент - ./арр/AddCheeseForm. tsx: "use cli.ent"; i.мрогt { addCheeseActi.on} fгOl'I "./addCheeseActi.on"; functi.on AddCheeseForм() { гetuгn ( <fогм acti.on={addCheeseActi.on} мethod="post"> <i.nput type="text" naмe="cheese" /> <button type="suЬl'li.t">Add Cheese</button> ехрогt </forм> ); } Теперь, когда это клиентский компонент, мы можем выполнять интерактивные действия, например реагировать на изменения состояния формы.
Фреймворки 1 289 Давайте для этого обновим нашу форму AddCheeseForм: "use cli.ent"; { addCheeseActi.on} fгOl'I "./addCheeseActi.on"; { useFor111Status } fгOl'I "геасt-dом"; i.мрогt i.мрогt funcnon AddCheeseForм() { const { pendi.ng} = useForмStatus(); ехрогt retuгn ( acti.on={addCheeseActi.on} мethod="post"> <input di.saЫed={pendi.ng} type="text" naмe="cheese" /> <button type="suЬмi.t" di.saЫed={pendi.ng}> {pendi.ng ? "Loadi.ng ... " : "Add Cheese"} </button> <foгPI </fOГPI> ); } Поскольку наша форма AddCheeseForм является клиентским компонентом, мы можем использовать useForмStatus для получения статуса формы. Это хук, предоставлен­ ный React. true, равно Этот хук возвращает объект со свойством когда форма отправляется, и false, pendi.ng, значение которого когда форма не отправляется. Мы можем применить его, чтобы отключить форму на время ее отправки и показать индикатор загрузки. Теперь мы можем использовать эту форму на нашей странице, которая является серверным компонентом, примерно так: i.мрогt i.мрогt { Cheeseli.st} fгOl'I "./Cheeseli.st"; { AddCheeseForм} fгOl'I "./AddCheeseForм"; default funcnon CheesePage() { ( <di.v> <h1>Thi.s мi.ght sound cheesy, but I thi.nk <CheeseLi.st /> <AddCheeseFom / > </di.v> ехрогt гetuгn уоu'ге really grate!</h1> ); } В результате мы получили сочетание серверных и клиентских компонентов. Веб­ страница CheesePage и список Cheeseli.st являются серверными компонентами, а
Глава 290 8 форма AddCheeseForм - клиентским компонентом. Компоненты могут использовать­ ся повторно и в других частях нашего приложения. Существуют некоторые прави­ ла и соображения, касающиеся клиентских и серверных компонентов, но мы рас­ смотрим их в главе 9. На данный момент, если мы сфокусируемся на аналогичные проблемы подобно Remix. Next.js, то увидим, что он решает То же делает и наша собственная реализа­ ция маршрутизации, извлечения и мутации данных на основе файловой системы. Это делается немного по-другому, но основные механизмы в чем-то схожи. В идеале, изучив оба этих фреймворка, мы сможем понять, с какой целью мы об­ ращаемся к ним и какие проблемы они решают в наших интересах. Давайте в заключение поговорим о том, как выбрать фреймворк. Выбор фреймворка Решение о том, какой фреймворк React использовать для вашего проекта, может оказаться непростым, поскольку каждый из них предлагает свой набор функций, преимуществ и компромиссов. В этом разделе мы попытаемся дать некоторое представление о том, что именно делает популярные фреймворки React приемле­ мым вариантом для разработчиков сегодня. Мы обсудим такие факторы, как ско­ рость обучения, гибкость и производительность. Сведения об особенностях фрейм­ ворков могут помочь вам выбрать наиболее подходящий из них для ваших конкретных нужд. Стоит отметить, что один фреймворк по своей сути не лучше и не хуже другого. Каждый фреймворк имеет собственный набор сильных и слабых сторон, и наилуч­ ший фреймворк для вашего проекта будет зависеть от ваших конкретных требова­ ний и предпочтений. Понимание потребностей вашего проекта Прежде чем мы углубимся в детали каждого фреймворка, важно понять конкрет­ ные потребности вашего проекта. Вот несколько важных вопросов, на которые сле­ дует обратить внимание: ♦ Каков масштаб вашего проекта? Это небольшой персональный проект, прило­ жение среднего размера с несколькими функциями или крупномасштабное сложное приложение? ♦ Какие основные функциональные возможности вы хотите включить в свой проект? ♦ Требуется ли вам рендеринг на стороне сервера (SSR), генерация сайта (SSG) или их комбинация? ♦ Создаете ли вы сайт с большим количеством контента, например блог или сайт электронной коммерции, который может выиграть от превосходного SEO?
Фреймворки ♦ 291 Являются ли важной частью вашего приложения данные в реальном времени или высокодинамичный контент? ♦ Какая гибкость вам требуется в плане настройки и контроля процесса сборки? ♦ Насколько важны производительность и быстродействие вашего приложения? ♦ Каков ваш уровень владения ♦ React и общими концепциями веб-разработки? Кто ваши целевые пользователи? Предприимчивые люди, сидящие за компью­ терами с быстрым доступом в Интернет? Или широкая публика с таким же ши­ роким спектром устройств и скоростей работы в Интернете? Ответы на эти вопросы дадут вам более четкое представление о том, что вам нужно от фреймворка. Next.js Давайте рассмотрим некоторые из этих параметров в контексте Next.js: Процесс обучения. Next.js "под капотом" использует передовые технологии React, часто прибегая к "канареечным релизам" 2 . Это означает, что Next.js нередко опережает время и поэтому может быть немного сложнее в освоении. Тем не менее команда Next.js проделала отличную работу по документированию фреймворка и предоставле­ нию четких руководств по различным функциям, которые помогут вам быстро приступить к работе. Гибкость. Next.js разработан с учетом требования гибкости между статическим и сервер­ ным отображением содержания. Он также полностью поддерживает клиентские приложения, Next.js хотя это не является его основным вариантом использования. также предоставляет богатую экосистему подключаемых модулей и ин­ теграций, которые могут значительно ускорить процесс разработки. Производительность. Next.js отдает производительности повышенный приоритет с акцентом на стати­ ческую генерацию, рендеринг на стороне сервера и кеширование. На момент на­ писания книги Next.js поставляется с четырьмя различными специализирован­ ными кешами, каждый из которых предназначен для обеспечения наилучшей производительности в разных вариантах использования. Однако это может вести к издержкам в виде разногласий по поводу границ между клиентом и сервером и осложнить принятие решений о том, когда что использовать. 2 Канареечный релиз (canary release)- метод снижения риска внедрения новой версии продукта в промышленную эксплуатацию путем предоставления изменений небольшому подмножеству пользователей. С течением времени изменения становятся доступны все большему числу пользова­ телей, если с релизом все хорошо. В конечном счете новую версию продукта получают все пользо­ ватели. - Прим. пер.
Глава 292 8 Также стоит отметить, что некоторые члены команды, создающей в компании Vercel, где разрабатывается ную обратную связь между Next.js и Next.js, React. React, работают что предполагает чрезвычайно тес­ Remix По сравнению с вавшая около Next.js, Remix l О лет назад. Она зто новая разработка на платформе была представлена создателями React, старто­ React Router и ак­ центирует внимание на фундаментальные веб-принципы, делая меньше допущений и обеспечивая большую гибкость. Процесс обучения. У Remix может быть немного более плавная кривая обучения, поскольку он в большей степени опирается на основы веб-платформы и использует React так, как многие привыкли до того, как разработчики стали уделять больше внимания серверным компонентам. Интуитивность. Remix часто выходит за свои рамки и позволяет использовать основы веб­ платформы, чтобы показать себя с лучшей стороны. Это может оказаться палкой о двух концах: с одной стороны, зто здорово, потому что интуитивно понятно и привычно, но, с другой стороны, зто может немного расстраивать, потому что зто не так "волшебно", как другие фреймворки. Производительность. Уникальный подход Remix к маршрутизации и загрузке данных делает его эф­ фективным и результативным. Поскольку выборка данных привязана к маршру­ там, извлекаются только необходимые данные для конкретного маршрута, что снижает общую потребность в данных. Кроме того, его оптимистичный подход к обновлениям пользовательского интерфейса и прогрессивные стратегии улуч­ шения совершенствуют качество взаимодействия с пользователем. Компромиссы Выбор фреймворка не обходится без компромиссов, суть которых заключается в соотношении удобства и контроля. Все фреймворки за счет упрощения процедур устраняют большую часть умственной работы и облегчают принятия решений, свя­ занных с нашими приложениями. Например, фреймворки по умолчанию содержат ответы на такие вопросы: ♦ Как мы выполняем маршрутизацию? ♦ Куда отправляются статические ресурсы? ♦ Должен ли сервер выполнять рендеринг? ♦ Откуда мы получаем данные?
Фреймворки 293 Учитывая, что фреймворки так сильно стандартизируют эти и другие действия, мы как разработчики дистанцируемся от контроля. Взамен мы получаем довольно мно­ го возможностей для продвижения вперед и потенциал для работы над более важ­ ными аспектами наших приложений, такими как бизнес-логика. Большинство, если не все, компромиссы в отношении фреймворков связаны с этим континуумом. Итак, как правильно выбрать фреймворк? Все зависит от потребностей вашего про­ екта и личных предпочтений. ♦ Если вам нужен достаточно гибкий универсальный фреймворк, Next.js, возмож­ но, вам подойдет больше, поскольку позволяет выбирать между статическим, серверным или полностью клиентским приложением. ♦ Если вы предпочитаете серверный, постепенно расширяющийся подход, осно­ ванный на принципах веб-разработки, Remix может стать вашим лучшим вы­ бором. В любом случае, рекомендуется опробовать один из них (или оба) для небольшого проекта или части вашего приложения. Это поможет вам лучше понять, как фрейм­ варки работают и с каким из них вам работать наиболее комфортно. Опыт разработчиков Обе платформы предлагают опыт разработчиков мирового уровня, уделяя особое внимание производительности и простоте использования. Как мы уже видели ранее в этой главе, оба фреймворка предоставляют богатый набор функций и инструмен­ тов, помогающих разработчикам создавать высококачественные приложения. Производительность сборки становится все более важной по мере роста сложности и размера проекта. И в Next.js, и в Remix были проведены оптимизации для сокра­ щения времени сборки. В Next.js по умолчанию используется статическая генерация, что означает предва­ рительный рендеринг страниц во время сборки. Это может привести к более быст­ рой загрузке страниц, но и к увеличению времени сборки, особенно для сайтов с большим количеством страниц. Для того чтобы решить эту проблему, в регенерация Next.js введена инкрементная статическая (incremental static regeneration, ISR), позволяющая разработчикам вос­ станавливать статические страницы после их создания без полной повторной сбор­ ки. Эта функция может значительно сократить время создания больших динамич­ ных сайтов. Remix, с другой стороны, обладает уникальным подходом к производительности сборки. Он использует серверную архитектуру, и значит, страницы отображаются сервером по запросу, а НТМL-код отправляется клиенту.
Глава 294 8 Производительность во время выполнения Как Next.js, так и Remix разработаны с заботой о хорошей производительности и предлагают несколько оптимизаций для создания быстрых и отзывчивых приложений. Next.js поставляется с несколькими встроенными функциями оптимизации произ­ водительности. Он поддерживает автоматическое разделение кода, что гарантирует загрузку только необходимого кода для каждой страницы. Он также имеет встро­ енный графический компонент, который оптимизирует загрузку изображений для повышения производительности. Гибридная модель SSG/SSR в Next.js позволяет разработчикам выбирать оптималь­ ную стратегию получения данных для каждой страницы, обеспечивая баланс про­ изводительности и свежести данных. Страницы, которые не требуют свежих дан­ ных, могут быть предварительно обработаны во время сборки, что ускоряет загрузку страницы. Для страниц, которым нужны обновленные данные, можно ис­ пользовать рендеринг на стороне сервера или Next.js ISR. также обеспечивает автоматическую статическую оптимизацию страниц без блокирования запросов данных, гарантируя, что страницы будут представлены в виде статических НТМL-файлов, что сокращает время до первого байта (time to first byte, TTFB). Наконец, нентов Next.js по возможности использует все преимущества серверных компо­ React, позволяя отправлять клиенту меньше JavaScript-кoдa, что приводит к более быстрой загрузке страниц и минимизации других накладных расходов. Remix использует несколько иной подход к повышению производительности. Вме­ сто предварительной обработки страниц он применяет серверный рендеринг, пере­ давая только тот НТМL-код, который нужен клиенту. Это может привести к более быстрому TTFB, особенно для динамического контента. Одной из ключевых особенностей Remix является его надежная стратегия кеширо­ вания. В нем используются нативные АРУ-интерфейсы выборки и кеширования браузера, что позволяет разработчикам определять стратегии кеширования для раз­ личных ресурсов. Это ускоряет загрузку страниц и повышает устойчивость прило­ жения. И Next.js, и Remix обладают неоспоримыми преимуществами для крупномасштаб­ ных и сложных проектов. Они оба концентрируют опыт многих разработчиков, улучшая производительность сборки и время выполнения. Next.js - возможно, лучший выбор, если вы предпочитаете зрелую экосистему с расширенными воз­ можностями и подключаемыми модулями, гибридную модель ционные функции, такие как ISR. С другой стороны, Remix SSG/SSR и иннова­ может быть более под­ ходящим, если вам нравится серверный рендеринг с мгновенным разворачиванием. Этот фреймворк имеет сильный акцент на том, чтобы использовать особенности веб-платформы, такие как двинутые концепции API выборки данных и кеширования, React, как Suspense и Server Components. а также такие про­ Выбор наиболее подходящего фреймворка для вашего конкретного проекта в ко­ нечном счете будет зависеть от опыта вашей команды, требований к проекту и ва-
Фреймворки 295 ших предпочтений в отношении определенных архитектурных паттернов. Незави­ симо от выбора, и Next.js, и Remix являются прочной основой для создания высо­ кокачественных и производительных приложений React. Обзор главы В ходе этой главы мы углубились в концепцию фреймворков React. Эта глава по­ зволила нам изучить основополагающие принципы, логику и практический смысл использования фреймворков. Обсуждение началось с краткого обзора конкурентного React и его влияния на эф­ фективность рендеринга и интерактивность пользователей. Затем мы перешли к изучению того, "зачем" и "для чего" используются фреймворки React: почему они необходимы, какие преимущества они предоставляют и какие компромиссы влекут за собой. Мы сделали это, внедрив наш собственный базовый фреймворк, который позволил нам понять механизмы и концепции, лежащие в основе фреймворков React. Затем мы изучили концепцию маршрутизации на основе файловой системы, которая яв­ ляется общей особенностью многих фреймворков React. Мы также рассмотрели выборку данных и то, как ее можно реализовать во фреймворке. Затем мы углубились в сравнение различных фреймворков, сосредоточив внимание в первую очередь на Next.js и Remix. Каждый фреймворк предлагает свой уникаль­ ный набор функций и преимуществ, и выбор часто зависит от конкретных потреб­ ностей проекта. Мы изучили, как эти фреймворки решают задачи серверного рен­ деринга, маршрутизации, выборки и мутации данных, и как они соотносятся с нашей собственной реализацией этих функций. Благодаря этому мы получили представление о механизме фреймворков, выявив общие черты между нашей собственной реализацией и фреймворками. Мы также рассмотрели компромиссы, связанные с использованием фреймворков, и то, как их можно смягчить, поняв лежащие в их основе механизмы. Наконец, мы обсудили, как выбрать фреймворк, и рассмотрели некоторые компро­ миссы, участвующие в принятии этого решения. Мы также изучили опыт разработ­ чиков и производительность фреймворков во время выполнения приложения и рас­ смотрели, какие варианты могут быть лучшими для наших проектов. Проверьте ваши знания В завершение этой главы мы зададим несколько вопросов, которые помогут вам проверить знания концепций, которые мы рассмотрели: 1. Каковы основные цели использования фреймворков Remix, и какие преимущества они дают? React, таких как Next.js или
296 2. Глава 8 Каковы некоторые компромиссы или недостатки использования фреймворков React? 3. Какие распространенные проблемы решаются с помощью фреймворков? 4. Каким образом фреймворки справляются с этими проблемами? Что дальше? В этой главе мы вкратце упомянули серверные компоненты React и в общих чертах рассказали о них. В следующей главе мы уделим больше внимания серверным компонентам React и погрузимся еще немного глубже: разберемся, что в них цен­ ного и каков принцип их работы, написав минимальный сервер, который отобража­ ет и обслуживает серверные компоненты React. Кроме того, мы рассмотрим, почему серверные компоненты React требуют нового поколения инструментов для сборки, таких как упаковщики, маршрутизаторы и многое другое. В конечном счете мы выйдем на углубленное понимание серверных компонентов React и лежащего в их основе механизма информативным и познавательным погружением. - это, несомненно, будет
ГЛАВА Серверные компоненты В предыдущей главе мы погрузились в мир фреймворков внимание Next.js и Remix. React, 9 React уделив особое Мы рассмотрели причины, по которым вы, возможно, решите использовать фреймворк. К таким причинам в первую очередь следует от­ нести преимущества абстрагирования, соглашений, ускоряющих разработку, пред­ лагаемые комплексные решения, а также общее влияние этих факторов на повыше­ ние производительности приложений. Мы углубились в детали Remix и Next.js, продемонстрировав общие подходы, ис­ пользуемые каждым фреймворком для решения схожих проблем путем внедрения нашего собственного элементарного скелетного фреймворка, и упомянули сервер­ ное направление Next.js, Server Components, RSC). работающее с серверными компонентами React (React Если говорить о RSC, то эти компоненты представляют собой интересную тенден­ React, направленную на повышение производительности, резуль­ удобства использования приложений React. Эта усовершенствованная цию в экосистеме тативности и архитектура сочетает в себе преимущества (multipage apps, МРА) с серверным (single-page application), обеспечивая многостраничных приложений рендерингом и клиентского рендеринга SPA удобство работы с пользователем без ущерба для производительности или удобства обслуживания. В этой главе мы рассмотрим основные концепции и лежащие в их основе ментальные модели и механизмы, на базе которых работают RSC. Самую свежую информацию react.dev (https://react.dev/). на этот счет вы всегда можете получить на сайте RSC представляет новый тип компонентов, которые "запускаются" на сервере и поэтому исключаются из клиентского пакета JavaScript. Эти компоненты могут стартовать во время сборки, позволяя вам выполнять чтение из файловой системы, извлекать статическое содержимое или получать доступ к нужному уровню дан­ ных. Передавая данные в качестве пропсов из серверных компонентов в интерак­ тивные клиентские компоненты в браузере, RSC поддерживают высокую эффек­ тивность и быстродействие приложения. Как же работают серверные компоненты? Для того чтобы лучше это понять, давай­ те немного углубимся в суть серверных компонентов.
Глава 298 9 Как только что бьmо описано, серверный компонент это особый тип компонента, - который выполняется только на сервере. Для того чтобы лучше понять это, давайте вспомним, что компонент React - это React: const COP1ponent = () => <dtv>hi.!</dtv>; не что иное, как функция, которая возвраща­ ет элемент В этом фрагменте COP1ponent- это функция, которая возвращает <di.v>hi.!</di.v>. Ко­ <di.v>hi.!</di.v> возвращает другой элемент React, поскольку< в React являет­ ся псевдонимом для React.createElef'IE!nt. Мы рассматривали это в главе 2, посвя­ щенной JSX. Если вы что-то забыли, сейчас самое время быстро обновить эту нечно, информацию и вернуться обратно. В конечном счете все компоненты возвращают элементы React, которые составляют виртуальный DOM. Серверные компоненты ничем особенным в этом смысле не отличаются. Не важно, где компонент выполняется В главе 3 - на сервере или на клиенте, мы видели, что элементы React - - он возвращает vDOM. это просто Jаvа-объекты, имеющие следующую схему: { $$typeOf: SyмЬol("react.elef'IE!nt"), type : о => ( { $$type0f: SyмЬol("react.eleмent"), type: "di.v", ргорs: { chi.ldгen: [ { $$type0f: SyмЬol("гeact.eleмent"), ргорs: { chi.ldren: "hi.!" }, }, ], }, }), } Как показано, вызов нашей функции среде вернет элемент COP1ponent как в клиентской, так и в серверной React. В случае серверных компонентов они выполняются (вызываются) только на серве­ ре, и результирующий объект JavaScript, представляющий элемент, отправляется клиенту по сети. Конечно, клиентские компоненты React, к которым мы привыкли. - это обычные компоненты
Серверные компоненты React 1 299 Преимущества Понимая все вышесказанное, мы начинаем видеть некоторые преимущества сер­ верных компонентов. ♦ Они выполняются только на стороне сервера, т. е. на компьютерах, вычисли­ тельную мощность которых мы контролируем. Это ведет к более предсказуемой производительности, поскольку мы не выполняем вычисления на незнакомых и непредсказуемых клиентских устройствах. ♦ Они выполняются в наших защищенных серверных средах, поэтому мы можем выполнять безопасные операции в серверных компонентах, не беспокоясь об утечке токенов и другой защищенной информации. ♦ Серверные компоненты могут быть асинхронными, поскольку на наших серве­ рах мы можем ждать завершения их выполнения, прежде чем мы поделимся ими с клиентами по сети. В этом заключается истинная мощь серверных компонентов. Далее давайте рас­ смотрим, как серверные компоненты взаимодействуют с рендерингом на стороне сервера. Серверный рендеринг В предыдущих главах мы подробно рассматривали рендеринг на стороне сервера, поэтому здесь не будем углубляться в детали. Вместо этого мы сосредоточимся на взаимодействии серверных компонентов и серверного рендеринга. По сути, серверные компоненты и серверный рендеринг можно рассматривать как два отдельных независимых процесса, где один процесс заботится исключительно о React, а дру­ элементов React и рендеринге компонентов на сервере и генерировании дерева элементов гой процесс - серверный рендеринг - использует это дерево преобразует его в НТМL-разметку, которая может быть передана клиентам по сети. Если мы рассмотрим оба процесса, один из которых предназначен для преобразо­ вания компонентов в элементы React React, а другой - для преобразования элементов в НТМL-строки или потоки, мы начнем понимать, как эти две технологии сочетаются друг с другом. Давайте назовем первый процесс RSС-рендеринго.м, ко­ торый превращает серверные компоненты в дерево элементов цесс - серверны.м рендерингом, который превращает элементы React, а второй про­ React в поток HTML. Исходя из этого, взаимодействие между серверными компонентами и серверным рендерингом можно понять следующим образом: 1. На сервере дерево Вот дерево JSX: <di.v> <h1>hi.!</h1> JSX преобразуется в дерево элементов.
Глава 300 9 <p>I like React!</p> </di.V> Оно преобразуется в дерево из трех элементов: { $$typeOf: Sy111Ьol("react.elerrient"), type: "div", props: { chHdren: [ { $$typeOf: Sy111Ьol("react.elerrient"), type: "hl", props: { chHdren: "hi!" } }, { $$typeOf: Sy111Ьol("react.elerrient"), type: "р", props: { chHdren: "I Hke React!" } }, ], }, } 2. На сервере это дерево элементов затем преобразуется в строку или поток. 3. Результат отправляется клиенту в виде большого строкового объекта 4. React JSON JSON. на стороне клиента может, как обычно, прочитать этот обработанный и отобразить его. Если мы представим все это как код на стороне сервера, он будет выглядеть при­ мерно так: // seгveг.js const express = require("express"); const path = require("path"); const React = require("react"); const ReactDOМServer = require("react-d0111/server"); const Арр = require("./src/App"); const арр = express();
Серверные компоненты React 301 app.use(express.stati.c(path.joi.n(_di.rnal'le, "bui.ld"))); app.get("*", async (req, res) => { // Э,,,о секреРlный соус const rscTree = awatt turnServerCOl'lponentslntoTreeOfElel'lents(<App />); // Э,,,о секреР1ный соус // Преобразуем ожидае№Ые серверные компоненf'IЬI в сР1року const htмl = ReactDOМServer.renderToStri.ng(rscTree); // ОР/правим res.send(' ЭР/О <!ООСТУРЕ htмl> <htмl> <head> <ti.tle>My React App</ti.tle> </head> <body> <di.v i.d="root">${htмl}</di.v> <scri.pt src="/stati.c/js/мai.n.js"></scri.pt> </body> </htмl> '); }); app.li.sten(3000, () => { console.log("Server li.steni.ng on port 3000"); }); Этот фрагмент серверного кода взят непосредственно из главы серверную часть React, 6, где мы обсуждали за исключением того, что мы добавили шаг для обработки серверных компонентов перед передачей их в средство визуализации на стороне сервера - второй процесс в нашем примере. Логически именно так серверные компоненты и рендеринг на стороне сервера со­ четаются друг с другом: это взаимодополняющие процессы. Опять же, стоит отметить, что мы используем ных целях и, как упоминалось в главе 6, renderToStri.ng только в иллюстратив­ в подавляющем большинстве случаев лучше использовать более асинхронный, прерываемый API, такой как renderToPi.peaЫeStreaм или аналогичный. Теперь, когда мы понимаем взаимодействие между серверным рендерингом и сер­ верными компонентами, давайте немного углубимся в эту волшебную функцию
Глава 302 9 turnServerCOl'lponentslntoTreeOfEler1ents, которую мы вызывали в предыдущем фраг­ менте кода. Что она делает? Как это превращает серверные компоненты в дерево элементов? Является ли она средством визуализации React? Давайте это выясним. "Под капотом" Короткий и, возможно, слишком упрощенный ответ ных компонентов React. в дерево элементов - Оно рекурсивно работает с деревом как <Арр />, это - своего React, да, преобразование сервер­ рода средство визуализации начиная с верхнего уровня, такого и рекурсивно вызывает каждый компонент, чтобы получить из него возвращаемый элемент React (т. е. обычный объект JavaScript). Давайте представим примерную реализацию этого, а затем обсудим, что делает эта функция: async funcrton turnServerCOl'lponentslntoTreeOfElel'lents(jsx) { i.f ( typeof typeof typeof jsx == jsx === jsx === jsx === nu\\ "stгlng" 11 "nu111Ьег" 11 "Ьoolean" 11 ) { // Нам не f'lpeбyel'/Cя return jsx; ничего особенного делаf'lь с Эf'/uми f'lunaмu. } i.f (Array.tsArray(jsx)) { // ОбрабОl'lка каждого элеменf'/й в массиве. return awai.t PrOl'ltse.all(jsx.111ap(renderJSXT0CltentJSX(chtld))); } // Если ~ имеем дело с обьекf'/ОМ i.f (jsx != nu\\ && typeof jsx === "object") { // Если Эf'/О элеменf'I React, i.f (jsx.$$typeof === Sy111Ьol.for("react.eleмent")) { // '{ type} - 3f'IO Cf'lpoкa для всf'lроенных компоненf'lов. i.f (typeof jsx.type === "strtng") { // 3f'ю всf'lроенный компоненf'I наподобие <dtv />. // Пройдем по его пропсам, чf'lобы убедuf'lься, Чf'IO они return { ... jsx, ргорs: aw1i.t гenderJSXToCltentJSX(jsx.props), }; } преобраэуе№ в JSON.
Серверные компоненты i.f (typeof jsx.type === "functi..on") { // ЭР/о пользовамельский компоненм React наподобие (like / / Вызовем его функцию и повморим процедуру для JSX, // коморый она возврамим. const Сомроnепt = jsx.type; const props = jsx.props; const returnedJsx = awai.t Coмponent(props); return awai.t renderJSXToCli..entJSX(returnedJsx); <Footeг React 1 303 />). } throw new Error("Not i..мple~nted."); } else { // ЭР/о произвольный объекм (пропсы или чмо-мо внумри их). // ЭР/о объекм, но не элеменм React (1,,t,1 обрабамывали эмом случай // Просмомрим каждое значение и обрабомаем в нем любой JSX. return Object.froмEntri..es( awai.t Proмi..se.all( Object.entri..es(jsx).мap(async ([propNa~, value]) => [ вше). ргсэрNаме, awai.t renderJSXToCli..entJSX(value), ]) ) ); } } throw new Error("Not i..мpleмented"); } Хотя этот фрагмент может показаться немного пугающим, давайте внесем ясность: это просто большое дерево i..f/else, которое возвращает данные на основе своих аргументов. Давайте пройдемся по ветвям и поймем, что происходит, начиная с входного аргумента jsx. Для первой ветви, если мы рассмотрим такой элемент React, как этот: <di.v>hi!</di.v> дочерний элемент "ht!" - это просто строка. Если мы передаем эту строку нашему серверному компоненту для визуализации, мы хотим вернуть строку как есть. Идея состоит в том, чтобы возвращать типы объектов, которые стороне клиента и на стороне сервера. React React может понимать на может понимать и отображать строки, числа и логические значения как на стороне клиента, так и на стороне сервера, по­ этому мы оставим их как есть. Далее, если у нас есть массив, давайте отобразим его и рекурсивно обработаем ка­ ждый элемент с помощью нашей функции.
Глава 304 9 Массивы могут быть группой дочерних элементов, например: <di.v>ht</di.v>, <h1>hello</h1>, <span>love u</span>, (props) => <р i.d={props.i.d}>loreм i.psuм</p>, ]; Фрагменты представляют дочерние элементы в виде массивов. Итак, давайте про­ сто аналогично обработаем их, рекурсивно вызывая нашу функцию для каждого дочернего элемента, и будем двигаться дальше. Далее начинается самое интересное: мы обрабатываем объекты. Давайте вспомним, что все элементы React это объекты, но не все объекты являются элементами React. Как мы узнаем, что объект является элементом React? У него есть свойство $$type0f со значением символа - в частности, Syмbol. for( 'react.eleмent' ). Поэтому мы проверяем, имеет ли объект такую пару ключ/значение, и если да, то обрабаты­ ваем его как элемент React. Мы сделаем это сейчас с помощью вот такого фрагмента: i.f (jsx.$$typeof === Syмbol.for("react.eleмent")) { i.f (typeof jsx.type === "stгing") { // Эfrlo компоненf'I, похожий на <div />. // Пройдем по его пропсам, Чf'lобы убедuf'lься, чf'lo гetuгn { ... jsx, props: awai.t renderJSXToCli.entJSX(jsx.props), они преобразуеf.'Ы в JSON. }; } i.f (typeof jsx.type === "functi.on") { // Эfrlo пользоваf'/еЛьский компоненf'I React наподобие (like <Footeг />). // Вызовем его функцию и повl'lорим процедуру для JSX, кОl'lорый она вoзвpal'IUl'I. const Coмponent = jsx.type; const props = jsx.props; const returnedJsx = awai.t Coмponent(props); гetuгn renderJSXToCli.entJSX(returnedJsx); } new Error("Not i.мpleмented."); } etse { // Эfrlo произвольный обьекf'I (пропсы или Чf'IO-f'IO внуl'lри их). // Просмоl'lрим каждое значение и обрабоf'lаем в нем любой JSX. thгow гetuгn Object.froмEntri.es( awai.t Proмi.se.all( Object.entri.es(jsx).мap(async ([ргорNаме, value]) => [
Серверные компоненты React 305 propNa111e, awatt renderJSXToCltentJSX(value), ]) ) ); } Внутри ветви true оператора Н мы выполняем еще одну проверку: является ли jsx. type React могут DOM, таких как строкой или функцией? Мы делаем это потому, что элементы иметь оба типа. Строки используются для встроенных элементов "dtv", "span" и т. д. Функции применяются для пользовательских компонентов, та­ ких как <Footer />. Если это строка, мы знаем, что это встроенный элемент DOM, поэтому мы можем просто вернуть его как есть. Однако мы должны рекурсивно вызвать нашу функцию для его пропсов поскольку его пропсы могут иметь до­ - черние элементы, которые являются конкурентными компонентами React. Если это функция, мы знаем, что это пользовательский компонент, поэтому мы вызываем эту функцию с пропсами. Мы вызываем нашу функцию рекурсивно с возвращае­ мым ею JSX в качестве аргумента, пока она, наконец, не вернет строку, число, ло­ гическое значение, массив этих типов или элемент React с типом strtng, который попадет в другую ветвь. Обратили внимание на awaH перед вызовом функционального компонента? По­ скольку это выполняется на стороне сервера, мы можем асинхронно ожидать функ­ циональный компонент в случае, если это серверный компонент! В этом и заклю­ чается волшебство серверных компонентов: мы можем асинхронно ожидать их на стороне сервера, и они вернут элемент который мы затем сможем передать в React, renderToStrtng или renderToPtpeaЫeStreaм, чтобы преобразовать его в направляемую клиенту строку или поток строк. На самом деле, это именно то, что делает наша функция: она просто ожидает выполнения всех асинхронных действий рекурсивно, чтобы создать дерево элементов (объект JavaScript) со всеми разрешенными зави­ симостями данных. Наконец, если объект не является элементом React, мы знаем, что это обыкновен­ ный объект, поэтому мы рекурсивно вызываем нашу функцию для каждого значе­ ния в объекте и возвращаем результат. Объект обычно представляет собой обыкно­ венные пропсы, поэтому в ветке else мы рекурсивно вызываем нашу функцию для каждого значения пропсов и возвращаем результат. Таким образом, мы эффективно разворачиваем любые компоненты, которые могут быть переданы как пропсы, ис­ пользуя паттерны наподобие render props, как обсуждалось в главе Вот и все! Это наше средство рендеринга RSC 5. в минимальном формате. Он не идеален, но это хорошее начало. Мы можем использовать его для преобразования наших серверных компонентов в элементы React, которые затем можно отправлять клиентам. Как только мы это сделаем, мы просто передадим результат в renderToStrtng или renderToPtpeaЫeStreaм, или даже сериализуем и отправим непосредственно в брау-
Глава 306 зер. React элементов 9 на стороне клиента сможет отобразить это, потому что это просто дерево React, которые понимает. Однако существует еще одна проблема, React которую нам необходимо рассмотреть: сериализация. Сериализация Все становится немного сложнее, когда мы пытаемся сериализовать элементы React. Сериализация элементов React является фундаментальным аспектом обеспе­ чения правильного и эффективного отображения вашего приложения при начальной загрузке, поскольку каждый вывод, отображаемый сервером, должен соответство­ вать клиентскому, чтобы React мог корректно согласовывать и дифференцировать данные. Когда ваше приложение отрисовывается на сервере, созданные элементы React необходимо преобразовать в НТМL-строки, которые можно отправить в браузер. Этот процесс превращения элементов React в строки называется сериали­ зацией. В типичном приложении создаются путем вызова React элементами React являются объекты в памяти. Они React.createEler,ent или с использованием синтаксиса JSX. Эти элементы представляют собой предполагаемый рендеринг компонента, но они еще не являются фактическими элементами DOM. Они больше похожи на инструк­ ции о том, как должен выглядеть DOM: const eler,ent = <h1>Hello, world</h1>; При рендеринге ReactDOМServer. на сервере renderToStri.ng, с элементы использованием React функции, подобной сериализуются в НТМL-строки. Этот процесс сериализации проходит по дереву элементов React, генерирует соответст­ вующий НТМL-код для каждого элемента и объединяет все это в общую НТМL­ строку: const ht~lStri.ng = ReactDOМServer.renderToStri.ng(eler,ent); // ht~lStrtng буде~ выгляде~ь как '<h1>Hello, world</h1>' Затем эту НТМL-строку можно отправить клиенту, где она будет использоваться в качестве начальной разметки для страницы. Как только пакет гружен на клиент, React сделает гидратацию DOM, JavaScript будет за­ подключив обработчики собы­ тий и любой динамический контент. Этап сериализации имеет решающее значение по нескольким причинам. Во­ первых, он позволяет серверу как можно быстрее отправить клиенту полную, гото­ вую к отображению НТМL-страницу. Это сокращает время загрузки страницы, по­ скольку пользователи могут быстрее начать взаимодействовать с содержимым. Кроме того, сериализация элементов React в НТМL-строку обеспечивает согласо­ ванную и предсказуемую начальную визуализацию независимо от среды. Получен­ ный НТМL-код является статичным и будет выглядеть одинаково независимо от того, отображается ли он на сервере или на клиенте. Такая согласованность необ­ ходима для обеспечения бесперебойной работы с пользователем, поскольку она предотвращает любое мерцание или изменение макета, которые могли бы возник­ нуть, если бы исходный рендеринг отличался от окончательного.
Серверные компоненты React 1 307 Наконец, сериализация облегчает гидратацию на стороне клиента. Когда пакет JavaScript загружается на клиент, React должен подключать обработчики событий и показать любой динамический контент. Наличие сериализованной НТМL-строки в качестве начальной разметки обеспечивает React прочную базу для работы, что де­ лает процесс гидратации более эффективным и надежным. Несмотря на то что нам нужно сериализовать компоненты в строки, мы не можем просто использовать объектами React JavaScript. JSON.stri.ngi.fy, т. к. элементы React не являются обычными Это объекты со специальным свойством $$typeof, которое использует для их идентификации, и значением этих свойств является сим­ вол. Символы не могут быть сериализованы и отправлены по сети, поэтому нам нужно сделать что-то еще. На самом деле все не так сложно благодаря встроенной поддержке сред выполне­ Node.js, где находится наш сервер. Эта встроен­ ная поддержка предоставляется нам в виде JSON. stri.ngi. fy и JSON. рагsе. Обе функции JSON, которыми являются ния JavaScript, включая браузер и рекурсивно сериализуют или десериализуют объекты React. Их API выглядит JSON.stri.ngi.fy(object, герlасег); JSON.parse(object, replacer); элементы где replacer - следующим образом: это функция, которая получает ключ и значение и может возвращать заменяющее значение при выполнении определенных условий. В нашем случае мы хотим заменить значение $$typeof на сериализуемый тип, например stri.ng. Вот как бы мы это сделали: JSON.strtngi.fy(jsxTree, (key, value) => { i.f (key === "$$typeof") { return "react.elerient"; // <- сf'/рока!! } return value; //<-возвращаем все другие значения как ecf'lь }); Вот и все! Мы закончили. Для того чтобы десериализовать это на стороне клиента, мы поступаем наоборот: JSON.parse(sertalizedJsxTree, (key, value) => { i.f (key === "$$typeof") { return Sy111Ьol.for("react.elel"IE!nt"); //<-символ!! } return value; //<-возвращаем все другие значения как ecf'lь }); И это все! Теперь мы можем сериализовать и десериализовать элементы React. Мы можем отображать серверные компоненты на сервере и отправлять их клиентам. Это позволяет выполнить нашу первую загрузку, однако нам все еще нужно обра­ батывать обновления и навигацию. Давайте сначала займемся навигацией, а с об­ новлениями разберемся позже.
Глава 308 9 Навигация Если в нашем приложении есть ссылка с подцержкой RSC, например, такая: <а href="/Ыog">Blog</a> нажатие на эту ссылку приведет к запуску полностраничной навигации, что заста­ вит браузер отправить запрос на сервер, который затем отобразит страницу и от­ правит ее обратно в браузер. Именно так много лет назад мы привыкли работать в мире РНР, и это сопряжено с определенными трудностями и ощущением медли­ тельности. Но мы можем добиться большего: с помощью RSC мы можем реализо­ вать мягкую навигацию, при которой состояние сохраняется между переходами по маршруту. Мы делаем это, отправляя серверу URL-aдpec, по которому хотим пе­ рейти, и сервер отправляет нам обратно дерево JSX для этой страницы. Затем в браузере повторно отображает страницу с новым деревом JSX, React и у нас будет но­ вая страница без обновления всей страницы. Это именно то, что мы собираемся сделать. Для этого нам нужно немного подправить наш клиентский код. Нам нужно доба­ вить слушателя событий ко всем ссылкам в нашем приложении, что предотвращает поведение ссылки по умолчанию и вместо этого отправляет запрос на сервер для новой страницы. Мы можем сделать это следующим образом: wi.ndow.addEventli.stener("cli.ck", (event) => { "if (event.target.tagNal'le !== "А") { return; } event.preventDefault(); navi.gate(event.target.href); }); Мы добавляем слушателя событий в wi.ndow из соображений производительности: мы не хотим добавлять слушателя к каждой отдельной ссылке в нашем приложе­ нии, поскольку большое количество слушателей может замедлить работу. Вместо этого мы добавляем в wi.ndow одного слушателя событий и проверяем, является ли целью нажатия ссылка. Это называется делегированием событий (event de/egation). Если пользователь все-таки нажимает на элемент А, мы отключаем поведение ссыл­ ки по умолчанию и вместо этого вызываем функцию навигации, которую мы опре­ делим через секунду. Эта функция отправит запрос на сервер для новой страницы, а затем React отобразит ее на клиенте. Давайте определим функцию навигации: async functi.on navi.gate(url) { const response = awai.t fetch(url, { headers: { "jsx-only": true} }); const jsxTree = awai.t response.json(); const ele111ent = JSON.parse(jsxTree, (key, value) => { i.f (key === "$$typeof") {
Серверные компоненты return React 1 309 Syl'lЬol.for("react.elel'lent"); } return value; }); root.render(elel'lent); } Здесь все довольно просто: мы отправляем запрос на сервер для новой страницы, десериализуем ответ в элемент root а затем отображаем этот элемент в корень React, нашего приложения. Это заставит React повторно обработать страницу с но­ вым деревом без обновления всей страницы. Но что такое нужно посмотреть на наш полный JSX, и у нас будет новая страница root? Для того чтобы понять это, нам клиентский JavaScript-фaйл: 1.l'lport { hydrateRoot} frOl'l "react-dol'l/cli.ent"; Vlport { deseгi.a li.ze } frOl'l ". / seri.a li.zer. js"; 1.l'lport Арр frOl'I "./Арр"; const root = hydrateRoot(docul'lent, <Арр wi.ndow.addEventLi.stener("cli.ck", (event) i.f (event.target.tagNal'le !== "а") { return; />); // <- э~о корень => { } event.preventDefault(); navi.gate(event.target.href); }); async functi.on navi.gate(url) { const response = awai.t fetch(url); const jsxTree = awai.t response.json(); const elel'lent = deseri.ali.ze(jsxTree); root.render(elel'lent); } Мы получаем корень от использовать этот React root React при первоначальной гидратации страницы и можем для отображения в него новых элементов. Вот так работает "под капотом". Мы просто применяем тот же API, который React использует внутри. Это хорошо: значит, мы не делаем ничего особенного или хакерского, мы просто используем общедоступный API React.
Глава 310 9 Наконец, нам нужно, чтобы в случае, если серверу задается заголовок отвечал только древовидным объектом JSX jsx-only, он для следующей страницы вместо того, чтобы отвечать полной строкой HTML. Мы можем сделать это следующим app.get("*", async (req, res) => { const jsxTree = awatt turnServerCOP1ponentslntoTreeOfEle1'1ents(<App />); образом: / / Э!'/0 и eCl'lb секреl'IНЫЙ соус tf (req.headers["jsx-only"]) { res.end( JSON.stringify(jsxTree, (key, value) => { tf (key === "$$typeof") { return "react.elel'lent"; } return value; }) ); } else { const htмl = ReactDOМServer.renderToString(jsxTree); res.send(' <!DОСТУРЕ htмl> <htмl> <head> <title>My React App</title> </head> <Ьоdу> <div id="root">${htмl}</div> <script src="/static/js/мain.js"></script> </body> </htмl> ' ); } }); Вы обратили внимание, что при наличии этого заголовка мы отправляем не просто строку? Делаем мы так потому, что нам нужно выполнить роне клиента, а бенность API, JSON.parse ожидает строку, а не объект JSON. JSON, а JSON.parse на сто­ Это всего лишь осо­ но это не так уж и плохо. Теперь у нас есть способ переходить на новые страницы без обновления всей стра­ ницы. Мы можем управлять навигацией в нашем приложении с поддержкой RSC.
Серверные компоненты React 311 Все переходы по якорным ссылкам выполняются плавно и без перерисовки всей страницы. Но как насчет обновлений? Как мы обрабатываем обновления? Давайте разберемся с этим в следующем разделе. Внесение обновлений Несмотря на массу положительных сторон ограничения, именно а дополнительные RSC, следует учитывать и некоторые интеллектуальные необходимостью думать о двух разных типах компонентов затраты, связанные (серверных с и клиент­ ских). Это связано с тем, что не все компоненты могут быть серверными. Например, рассмотрим простой компонент счетчика, который увеличивает значе­ 1, когда пользователь нажимает кнопку +: functi.on Counter() { const [count, setCount] = useState{0); return ( <di.v> <h1>Нello friends, look at ~У nice counter!</h1> <p>AЬout ~: 1 like pie! Sign ~У guest Ьооk!</р> <p>Count: {count}</p> <button onClick={() => setCount(count + 1)}>+</button> </di.v> ние счетчика на ); } Этот компонент никогда не сможет быть серверным по двум причинам. ♦ Он использует useState, который является API только клиентской части. Это оз­ начает, что сервер не знает, каково начальное значение параметра count, поэтому он не может отобразить исходный НТМL-код. Это проблема, поскольку серверу необходимо отобразить исходный НТМL-код, прежде чем клиент сможет при­ ступить к работе с интерактивным пользовательским интерфейсом. В серверной среде понятие "состояние" тами совместно. Однако в React, (state) до внедрения используется несколькими клиен­ RSC, состояние было локализова­ но в текущее приложение. Это различие представляет опасность. Оно может привести к путанице данных о состоянии между несколькими клиентами, что потенциально ведет к раскрытию конфиденциальной информации. Из-за этого несоответствия и связанных с ним угроз безопасности использование useState RSCs не поддерживают на стороне сервера. Это происходит потому, что состоя­ ние на стороне сервера принципиально отличается от состояния на стороне кли­ ента. Более того, функция-диспетчер (setState) из useState для отправки клиенту по сети должна быть сериализована, а функции не являются сериализуемыми объ­ ектами, поэтому это было бы невозможно.
Глава 312 ♦ 9 Этот компонент использует onCli.ck, который имеет смысл только для API кли­ ентской части. Это связано с тем, что серверы не интерактивны: вы не можете щелкнуть по запущенному процессу на сервере, поэтому компонентах - onCl i.ck в серверных это что-то вроде невозможного состояния. Более того, предпо­ лагается, что все пропсы для серверных компонентов должны быть сериализуе­ мыми, поскольку сервер должен иметь возможность сериализовать пропсы и от­ правлять их клиенту. Однако функции не могут быть сериализованы. Следовательно, если мы хотим использовать возможности серверных компонентов, код, который раньше был простым счетчиком, теперь тоже должен быть разделен на серверную и клиентскую части, вот так: // Серверный компоненf'I functton ServerCounter() { return ( <dtv> <h1>Нello fri.ends, look at му ni.ce counter!</h1> <р> rrie: I li.ke to count thi.ngs and I'м а counter and s~ti.rries I count thi.ngs but other ti.rries I enjoy playi.ng the Cello and one ti.rrie at band самр I counted to 1000 and а pi.rate арреагеd AЬout </р> <InteractlveCltentPart /> </dtv> ); } // Клиенf'/Скuй компоненf'I "use cli.ent"; functlon Interacti.veCli.entPart() { const [count, setCount] = useState(0); return ( <dtv> <p>Count: {count}</p> <button onCli.ck={() => setCount(count + 1)}>+</button> </dtv> ); } Это немного надуманный пример, но он иллюстрирует тот факт, что вы не можете просто взять любой компонент React и превратить его в серверный компонент. Вам нужно подумать о том, какие части вашего компонента могут отображаться на сер­ вере, а какие - на клиенте. Это создает некоторые трудности, поскольку, хотя в
Серверные компоненты 31 З React этом примере довольно очевидно, какие части могут быть отображены сервером, а какие - клиентом, в реальном приложении это может быть не так просто. Тем не менее рассмотрение этого примера в значительной степени полезно, потому что мы только что выделили крошечную часть нашего приложения, которая долж­ на быть интерактивной, и только эта часть приложения на самом деле будет дос­ тупна нашим пользователям как часть пакета следствие, мы отправляем по сети пакеты JavaScript, а остальное - нет. Как JavaScript значительно меньшего разме­ ра, что означает более быструю загрузку и более высокую производительность для наших пользователей как с точки зрения ресурсов процессора (поскольку для ана­ лиза и выполнения JavaScript требуется меньше работы), так и с точки зрения сети (поскольку требуется загружать меньше данных). Вот почему мы хотим сделать как можно больше безопасного рендеринга на серве­ ре, чтобы включать как можно меньше кода в клиентские пакеты. Что "под капотом"? Оставив в стороне дополнительные умственные затраты, давайте поговорим о том, как React разграничивает серверные и клиентские компоненты и работает с ними по отдельности. Это важно осмыслить, потому что это поможет нам понять, как вносить обновления в наше приложение. Компоненты cli.ent" обозначаются как клиентские путем добавления в начало файла, содержащего клиентский компонент. Для директивы RSC "use требуется инструментарий следующего поколения, позволяющий различать серверные и кли­ ентские компоненты на основе использования таких директив. С помощью упаковщика нового поколения разработчики пакетов могут создавать отдельные графы модулей для приложений React: серверный граф и клиентский граф. Серверный граф никогда не объединяется в пакет, поскольку он никогда не предоставляется пользователям, но все файлы, начинающиеся с директивы cli.ent", "use объединяются либо в общий клиентский пакет, либо в несколько пакетов для каждого компонента, которые могут загружаться с использованием ленивой загруз­ ки. Эта деталь реализации зависит от фреймворков, которые построены поверх RSCs. Итак, концептуально у нас есть серверный граф, который выполняется на сервере, и один или несколько клиентских пакетов, которые загружаются и когда они требуются клиенту. Но как React выполняются, узнает, когда импортировать и выпол­ нять клиентские компоненты? Для того чтобы понять это, нам нужно рассмотреть типичное дерево На рис. 9.1 React. Давайте сделаем это на нашем примере со счетчиком. мы представили дерево компонентов нашего приложения, в котором светлые компоненты отображаются на сервере, а темный - на клиенте. Поскольку корнем дерева является серверный компонент, все дерево отображается на сервере. Однако компонент Interacti.veCli.entPart является клиентским компонентом, поэто­ му он не отображается на сервере. Вместо этого сервер отображает заполнитель для клиентского компонента, который является ссылкой на конкретный модуль, соз­ данный компоновщиком клиентского пакета. Эта ссылка на модуль, по сути, гла-
Глава 314 9 сит: "Когда вы дойдете до этой точки в дереве, настанет время использовать вот этот конкретный модуль". <di.v> ( <hl> ( <р> ) ] <Cl-\.entPart/> <di.v> Рис. 9.1. Дерево компонентов, показывающее клиентский компонент и серверные компоненты Модуль не обязательно всегда загружается только в режиме ленивой загрузки, но может быть загружен и из исходного пакета, поскольку разработчики пакетов включают целую кучу модулей в пакеты, которые мы отправляем пользователям. Это может быть getModuleFrol'1BundleAtPosi.tt.on([0,4]) или что-то подобное. Суть в том, что сервер отправляет ссылку на нужный клиентский модуль, а React на сто­ роне клиента заполняет пробел. Когда это произойдет, React заменит ссылку на модуль реальным модулем из кли­ ентского пакета. Это небольшое упрощение, но его достаточно для объяснения ме­ ханизма. Затем клиентский компонент визуализируется на клиенте, и с ним можно взаимодействовать как обычно. Вот почему RSCs требуется упаковщик следующе­ го поколения: он должен иметь возможность создавать отдельные графы модулей для серверных и клиентских компонентов. На практике это означает, что в случае нашего примера счетчика сервер будет ото­ бражать следующее дерево: { $$typeof: Sy1'1bol(react.elel'1ent), type: "dt.v", props: { cht. ldren: [ { $$typeof: Sy1'1bol(react.elel'1ent), type: "hl", props: { cht.ldren: "Hello frt.ends, look at } }, 1'1У nt.ce counter!"
Серверные компоненты React { $$typeof: Syмbol(react.eleмent), type: 11 р 11 , props: { chi.ldren: About ме: I li.ke to count thi.ngs 11 11 } }, { // Заполниf'lель элеменf'/а ClientPaгt со ссылкой на модуль. // Oбpaf'lиf'le на Эf'IO внимание: Эf'IO ссылка на модуль! $$typeof: Syмbol(react.eleмent), type: { $$typeof: Syмbol(react.мodule.reference), default nаме: ./src/Cli.entPart.js fi.lenaмe: cli.ent-part-1234 мoduleld: 11 11 , 11 11 , 11 11 }, props: { chi.ldren: // ... ссылки на другие серверные компоненf'lы и клиенf'lские модули { $$typeof: Syмbol(react.eleмent), type: { $$typeof: SyмЬol(react.мodule.reference), default nаме: 11 11 11 fi.lenaмe: , ./src/AnotherCli.entCoмponent.js 11 }, props: { chi.ldren: [], } }, { $$typeof: Syмbol(react.eleмent), type: di.v props: { chi.ldren: ! ам а server coмponent 11 11 11 , 11 } } } } } } 1 315
316 Глава 9 Это дерево будет отправлено клиентской стороне, и, когда наружит ссылку на модуль, React React отрисует его и об­ разумно заменит ссылку на модуль фактическим модулем из клиентского пакета. Именно так React узнает, когда импортировать и запускать клиентские компоненты. Таким образом, мы можем видеть, что упаковщик по-прежнему может отрисовы­ вать целое дерево на сервере и оставлять только "дыры", которые необходимо за­ полнить на клиенте, при этом рекурсивно отображая даже дочерние компоненты клиентских компонентов на сервере, создавая полное дерево. Затем клиент запол­ няет все дерево, т. е. любые дыры, загрузкой и выполнением клиентских пакетов. Серверные компоненты также могут быть заключены в рамки ожидания Suspense, при этом фреймворки выполняют необходимую работу по передаче их пользовате­ лям со стороны сервера по мере их "готовности", т. е. асинхронно, по мере получе­ ния требуемых данных и выполнения других необходимых операций. Хорошо, надеюсь, теперь мы понимаем, как клиентские компоненты отделяются от серверных, что позволяет обновлять приложения, ориентированные на ентские компоненты, помеченные как "use cli.ent", RSCs. Кли­ могут без проблем содержать локализованные обработчики состояний и событий, такие как onCl i.ck. Учитывая, что мы теперь в основном разобрались с клиентскими компонентами и понимаем, как серверные компоненты выполняются на сервере и как клиентские компоненты включаются в клиентские пакеты, нам нужно обсудить некоторые ню­ ансы, связанные с этими темами. Нюансы Существует распространенное заблуждение, будто серверные компоненты выпол­ няются только на сервере, а клиентские - только на клиенте. Это не совсем так. Серверные компоненты действительно выполняются только на сервере и выводят объекты, представляющие элементы React, но клиентские компоненты не выпол­ няются исключительно на клиенте. Для того чтобы доскональное разобраться в этом, давайте обсудим, что вообще оз­ начает "выполнение компонентов". Когда мы говорим "компоненты выполняются", мы подразумеваем, что вызывается функция, представляющая компонент. Напри­ мер, предположим, что у нас есть такой компонент: functi.on MyCOl'lponent() { return <di.V>hello world</di.v>; } Когда мы говорим MyCOl'lponent "MyCol'lponent выполняется", мы подразумеваем, что функция вызывается со своими пропсами и возвращает элемент представляет собой простой объект { $$typeof: 5yl'1bol(react.elel'1ent), type: "di.v", JavaScript, React, который выглядящий следующим образом:
Серверные компоненты React 1 317 props: { chHdren: "hello world" } } Именно это мы имеем в виду, когда говорим "компоненты выполняются". Во время серверного рендеринга клиентские компоненты выполняются на сервере и выводят объекты, представляющие элементы зуются в строку HTML React. Затем эти элементы преобра­ и отправляются клиенту, где браузер отображает НТМL­ разметку. Таким образом, клиентские компоненты также выполняются на сервере, возвращают некоторые объекты, представляющие элементы преобразует их в HTML React, а затем сервер и отправляет клиенту. Для того чтобы получить более точное представление об этом, мы вправе сделать следующие утверждения: ♦ серверные компоненты выполняются на сервере, выводя объекты, представ­ ляющие элементы ♦ клиентские компоненты выполняются на сервере, выводя объекты, представ­ ляющие элементы ♦ React; React; на сервере существует большой объект, представляющий все элементы React и состоящий как из клиентских, так и из серверных компонентов; ♦ этот объект преобразуется в строку и отправляется клиенту; ♦ с этого момента серверные компоненты никогда не выполняются на клиенте; ♦ с этого момента клиентские компоненты выполняются исключительно на клиенте. Теперь границы выполнения как серверных, так и клиентских компонентов стано­ вятся более четкими. Возможно, здесь мы расходимся во мнениях, но стоит доба­ вить больше деталей, чтобы полностью понять и оценить взаимодействие между обоими типами компонентов. Правила работы серверных компонентов Теперь, когда мы поняли, как работают серверные компоненты, давайте обсудим некоторые правила, которым мы должны следовать при работе с ними, или, в более широком смысле, о чем следует помнить при работе с серверными компонентами. Главное - сериализуемость Для серверных компонентов все пропсы должны быть сериализуемыми. Это связа­ но с тем, что сервер должен иметь возможность сериализовать пропсы и отправлять их клиенту, как мы обсуждали ранее. Следовательно, в серверных компонентах пропсы не могут быть функциями или другими несериализуемыми значениями.
Глава 318 9 Это делает шаблон рендеринга пропсов, который мы обсуждали в главе 5, фактиче­ ски устаревшим. На данный момент, основываясь на нашем понимании того, как RSCs отображают­ ся на сервере и затем отправляются клиенту, мы должны понять, почему это прави­ ло существует. Допустим, у нас есть такой серверный компонент: functton ServerCo~ponent() { гetuгn <CltentCoriponent onCli.ck={O => alert("hi.")} />; } Выполнение его привело бы к ошибке. Однако мы могли бы обойти это, инкапсу­ лировав про пс onCl i.ck внутри Cli.entCooponent. Отсутствие эффективных хуков Серверная среда значительно отличается от клиентской. Она не интерактивна, у нее нет DOM и своего окна вывода. Поэтому эффективные хуки не поддерживаются серверными компонентами. В некоторых фреймворках, таких как Next.js, статический анализатор кода имеет правила, которые полностью запрещают все хуки в серверных компонентах, но они не обязательны. RSCs могут использовать хуки, которые не зависят от состояния, эффектов или браузерных АРI-интерфейсов. Например, хук useRef идеально подхо­ дит для использования в серверных компонентах, поскольку он не зависит от со­ стояния, эффектов или АРI-интерфейсов браузера. Однако, возможно, отсутствие разнообразия хуков не так уж и плохо, поскольку склоняет нас к более безопасной работе с компонентами. Состояние на самом деле не является состоянием Состояние серверных компонентов отличается от состояния клиентских компонен­ тов. Это связано с тем, что серверные компоненты отображаются на сервере, а кли­ ентские - на клиенте. Это означает, что состояние серверных компонентов может совместно использоваться сразу многими клиентами, поскольку отношения между сервером и клиентом являются отношениями в стиле широковещательной рассыл­ ки, а не одноадресной (один клиент, одно состояние), и, следовательно, высок риск утечки состояния между клиентами. В сочетании с правилом хуков это означает, что любые компоненты, которым тре­ буется состояние, получаемое посредством useState, useReducer или аналогичным способом, лучше всего подходят в качестве клиентских компонентов. Клиентские компоненты не могут импортировать серверные компоненты Клиентские компоненты не могут импортировать серверные компоненты. Это связа­ но с тем, что серверные компоненты выполняются только на сервере, а клиентские компоненты выполняются как на сервере, так и на клиенте, в том числе в браузерах.
Серверные компоненты React 319 Это означает, что если у нас есть клиентский компонент, подобный этому: "use cli.ent"; l.l'lport { ServerC01'1ponent} frOl'I "./ServerC01'1ponent"; functi.on Cli.entCoмponent() { return ( <di.v> <h1>Hey everyone, check out <ServerCOl'lponent /> </di.v> ~У great sегvег coмponent!</h1> ); } он может привести к ошибке, поскольку клиентский компонент пытается импорти­ ровать серверный компонент. Это невозможно, т. к. серверные компоненты выпол­ няются только на сервере. Серверный компонент, который мы импортируем, может дополнительно импортировать элементы, недоступные в клиентской среде выпол­ нения, например Node.js API. Это привело бы к ошибкам на клиенте. Серверный компонент мог бы выглядеть следующим образом: l.l'lport { readFi.le} frOl'I "node:fs/proмi.ses"; export async functi.on ServerC01'1ponent() { const content = awai.t гeadFi.le("./soмe-fi.le.txt", "utf-8"); return <di.v>{content}</di.v>; } Если мы попытаемся запустить этот код на стороне клиента, поскольку клиентский компонент импортировал его, мы получим сообщение об ошибке, т. к. функция ReadFi.le и модуль "node:fs/prOP1i.ses" недоступны в браузере. Вот почему клиентские компоненты не могут импортировать серверные компоненты. Однако клиентские компоненты могут создавать серверные компоненты с помо­ щью пропсов. Например, мы могли бы переписать наш клиентский компонент, чтобы он выглядел так: "use cli.ent"; functi.on Cli.entC01'1ponent({ chi.ldren }) { return ( <di.v> <h1>Hey everyone, check out ~У great {chi.ldren} </di.v> ); } sегvег cOl'1ponent!</h1>
320 Глава 9 и тогда в любом родительском серверном компоненте, содержащем этот клиент­ ский компонент, мы могли бы сделать следующее: viport { ServerCol'lponent} frOl'I "./ServerCOl'lponent"; async functton TheParentOfBothCol'lponents() { return ( <CltentCOl'lponent> <ServerCOl'lponent /> </CltentCOP1ponent> ); } Это сработало бы, потому что нет явного импорта серверного компонента из кли­ ентского компонента, но родительс~<ий серверный компонент передает серверный компонент в качестве пропса клиентскому компоненту. Единственная причина, по которой импорт запрещен, заключается в том, чтобы предотвратить возможность включения серверных компонентов в клиентский пакет, потому что компоновщики обращают внимание только на инструкции i.l'lport, а не на состав пропсов. Клиентские компоненты не так уж плохи Стоит отметить, что до появления серверных компонентов клиентские компоненты были единственным типом компонентов, которые мы имели в Это означает, React. что все наши существующие компоненты являются клиентскими компонентами, и это нормально. Клиентские компоненты денутся. Они по-прежнему - "хлеб и масло" - основа приложений React, и они никуда не React, и они по-прежнему явля­ ются наиболее распространенным типом компонентов, которые мы будем создавать. Мы упоминаем об этом здесь, потому что в этой теме возникла некоторая путани­ ца, и серверные компоненты воспринимаются некоторыми как превосходная заме­ на клиентским компонентам. Это не так. Серверные компоненты - это новый тип компонентов, которые мы можем использовать в дополнение к клиентским компо­ нентам, но они никоим образом не заменяют клиентские компоненты. Серверные действия Серверные компоненты RSCs - это мощная новая функция в также работают в паре с новой директивой "use React, но не единственная. sегvег", определяющей сер­ верные функции, которые могут быть вызваны из клиентского кода. Мы называем эти функции серверными действиями (server actions). Любая асинхронная функция может иметь своего тела, чтобы сигнализировать React "use sегvег" в качестве первой строки и компоновщику, что эта функция может быть вызвана из клиентского кода, но должна выполняться только на сервере. При
Серверные компоненты React 1 321 вызове серверного действия на клиенте серверу будет отправлен сетевой запрос, содержащий сериализованную копию всех переданных аргументов. Если серверное действие возвращает значение, это значение будет сериализовано и возвращено клиенту. Вместо того чтобы отдельно помечать функции надписью "use server", вы также можете добавить директиву в начало файла, чтобы помечать весь экспорт в этом файле как серверные действия, которые можно использовать где угодно, включая импорт в клиентский код. Формы и мутации В главе React 8 мы обсуждали, как Next.js и Remix обрабатывают формы и мутации. также добавляет (или уже добавил) первоклассные примитивы для них. Рас­ смотрим эту форму: // App.js async funcrton requestUsernal'le(forмData) { 'use server'; const usernal'le = forмData.get('usernal'le'); // } default Арр() { <fom action={requestUsernal'le}> <i.nput type="text" nal'le="usernal'le" /> <button type="suЬмH">Request</Ьutton> </fom> ехрогt } В этом примере requestUsernal'le - это серверное действие, передаваемое в <forм>. Когда пользователь отправляет эту форму, происходит сетевой запрос к серверной функции requestUsernal'le. При вызове серверного действия в форме React предоста­ вит ForмData формы в качестве первого аргумента серверного действия. Передавая серверное действие в действие формы, React может постепенно улуч­ шать форму. Это означает, что формы могут быть отправлены до загрузки пакета JavaScript. За пределами форм Стоит отметить, что серверные действия являются открытыми конечными точками сервера и могут вызываться в любом месте клиентского кода. При использовании серверного действия вне формы мы можем вызвать серверное действие при переходе, что позволяет нам отображать индикатор загрузки, показы-
322 Глава 9 вать оптимистичные обновления состояния и обрабатывать непредвиденные ошиб­ ки. Вот пример серверного действия вне формы: "use cli.ent"; V!poгt V!poгt i.ncref"lentli.ke fгOl'I "./acti.ons"; { useState, useTransi.ti.on} fгOl'I "геасt"; functton Li.keButton() { const [i.sPendi.ng, startTransi.ti.on] = useTransi.ti.on(); const [li.keCount, setli.keCount] = useState(0); const i.ncref"lentli.nk = async () => { "use server"; гetuгn li.keCount + 1; }; const onCli.ck = () => { startTransi.ti.on(async () => { // Для f'loгo чf'lобы npoчuf'laf'lь возвращаемое значение серверного дейсf'lвuя, //,.,.,,ожидаем возвраf'lа обещания (pгof'lise). const currentCount = awatt i.ncref"lentli.ke(); setli.keCount(currentCount); }); }; гetuгn <> <p>Total Li.kes: {li.keCount}</p> <button onCli.ck={onCli.ck} di.saЫed={i.sPendi.ng}> Li.ke </button>; </> ); } Таким образом, мы можем видеть, что серверные действия функция в React, - это новая мощная которая позволяет нам вызывать серверные функции из клиент­ ского кода. На самом деле это предназначено только для использования в библио­ теках или фреймворках, т. к. работа с этими директивами в ванильном React немно­ го громоздка и требует немалой работы по настройке. Однако это мощная функция, которая открывает множество интересных вариантов использования.
Серверные компоненты Будущее серверных компонентов Ожидается, что манда React RSCs 323 React React со временем будут развиваться и совершенствоваться. Ко­ активно работает над усовершенствованием реализации, устранением потенциальных проблем и расширением набора функций. Некоторые области те­ кущей разработки включают в себя следующие аспекты. Лучшая интеграция с пакетами. Команда React сотрудничает с разработчиками пакетов, чтобы обеспечить луч­ шую поддержку RSCs в таких пакетах, как Webpack, Rollup и др. Это упростит создание RSСs-совместимых платформ и приложений. Поддержка экосистемы. По мере развития вероятно, будет появляться все больше инструментов, RSCs, библиотек и фреймворков для поддержки и расширения этой новой архитектуры приложений. Это позволит разработчикам легче внедрять в свои проекты RSCs и получать выгоду от повышения производительности и результативности. RSCs представляют собой значительный шаг вперед в экосистеме React, предла­ гающий повышенную производительность, упрощенный процесс получения дан­ ных и улучшенный пользовательский опыт. Поскольку RSCs продолжают разви­ ваться и получают широкое распространение, ожидается, что они станут важным инструментом для создания современных, эффективных и удобных в использова­ нии приложений React. Благодаря такому глубокому пониманию RSCs вы теперь можете изучать и экспериментировать с этой передовой функцией в собственных проектах. Обзор главы В этой главе мы сосредоточили внимание исключительно на серверных компонен­ тах React (RSC), которые являются значительным достижением экосистемы React, направленным на повышение производительности, результативности и удобства использования приложений React. RSC представляют собой инновационную архи­ тектуру приложений, которая сочетает в себе лучшие качества многостраничных (multi-page apps, МРА), и одностраничных рендерингом (single-page applications, SPA). Такой под­ приложений с серверным рендерингом приложений с клиентским ход обеспечивает плавное взаимодействие с пользователем без ущерба для произ­ водительности или удобства обслуживания. Мы углубились в основные концепции, преимущества и лежащие в их основе ментальные модели и механизмы RSC. Клю­ чевым моментом стало внедрение нового типа компонента, который работает на сер­ вере, исключен из клиентского пакета JavaScript и может запускаться во время сбор­ ки. Это усовершенствование приводит к более эффективной структуре приложения. Стоит отметить, что на момент написания этой книги концепция чей" темой в области React RSCs была "горя­ и веб-инжиниринга, и некоторые детали, которые мы
Глава 324 рассмотрели, 9 возможно, изменились. react.dev (https://react.dev/) Как всегда, мы рекомендуем и различные каналы сообщества React посетить для получения самой свежей информации. Проверьте ваши знания 1. 2. Что является главной ценностью серверных компонентов React? Могут ли клиентские компоненты импортировать серверные компоненты? Объ­ ясните, почему? 3. Каковы компромиссы между серверными компонентами и традиционным кли­ ентским подходом Rеасt-приложений? 4. Что такое ссылки на модули и как React обрабатывает их 5. Как серверные действия делают приложения React во время согласования? более доступными? Что дальше? В следующей главе мы пойдем немного другим путем. До сих пор большую часть нашего путешествия мы провели, глубоко погружались в мир React, изучая его сложную внутреннюю работу, инновационные стратегии управления состоянием, возможности асинхронного рендеринга и, наконец, его мощные фреймворки. Те­ перь мы собираемся сделать шаг назад, чтобы расширить наш кругозор. Мы собираемся выйти за рамки React, чтобы познакомиться с альтернативными библиотеками и фреймворками, которые развивались параллельно с React, а иногда и в ответ на него доминирование. Эти альтернативы не только переняли некоторые из лучших функций React, но и внедрили собственные уникальные инновации, пре­ доставив нам новые захватывающие парадигмы и возможности в разработке поль­ зовательского интерфейса. В предстоящем исследовании мы проанализируем принципы работы и философию некоторых библиотек пользовательского интерфейса, таких как Qwik и Svelte. Vue, Angular, Solid, Мы рассмотрим их уникальные стратегии управления состоянием, обработки сторонних эффектов и как их можно сравнить с React с точки зрения производительности и опыта разработчиков. У каждого варианта есть свои минусы и плюсы, которые могут сделать ту или иную библиотеку более подходящей для определенных типов проектов или соответствующей предпочтениям разработчиков. Vue. Vue предлагает прогрессивную платформу, которая может быть адаптирована постепенно, т. е. вы можете начать с малого и постепенно внедрять все больше функций Vue по мере необходимости. Vue известен своим элегантным API и ориентирован на опыт разработчиков. В этой платформе представлена простая,
Серверные компоненты React 325 но мощная модель реактивности, основанная на его основной концепции реак­ тивных зависимостей, отслеживаемых во время рендеринга. Angular. Angular - это законченный, уверенный в себе фреймворк с более сложной сис­ темой обучения, но предлагающий надежные решения "из коробки". Его систе­ ма внедрения зависимостей и декларативные шаблоны предлагают иной подход к структуре приложения и управлению состоянием по сравнению с React. Solid. Solid - еще один ве JavaScript. Он претендент, который привлекает к себе внимание в сообщест­ обещает детализированную реактивность с использованием модели программирования, аналогичной и эффективную визуализацию. То, как React, но с акцентом на более быструю Solid отслеживает зависимости, может стать глотком свежего воздуха для разработчиков, стремящихся повысить эф­ фективность во время выполнения. Фрей.«ворк Qwik Qwik. представляет интересный подход, фокусируясь на оптимальной произво­ дительности загрузки с "предсказуемой" предварительной выборкой. Это дает уникальный взгляд на то, как мы можем структурировать и предоставлять наш JavaScript для оптимального взаимодействия с пользователем. Svelte. Svelte привлекает внимание, компилируя компоненты во время сборки в импе­ ративный код, который напрямую управляет DOM, что приводит к ускорению начальной загрузки и плавному обновлению. Модель реактивности, отмеченная реактивными инструкциями, представляет собой стратегией изменения виртуального DOM, интригующий принятой в контраст со React. Изучая эти фреймворки и библиотеки, мы будем использовать наши знания о React в качестве отправной точки. Это не только поможет нам лучше понять альтерна­ тивное программное обеспечение, но и углубит наше понимание React, предлагая точки сравнения и контрасты. Приготовьтесь познакомиться с уникальными подходами этих альтернативных библиотек пользовательского интерфейса к реактивности, управлению состоянием, побочным эффектам и многому другому. Изучая их, мы можем получить информа­ цию, которая поможет нам в решении проблем, независимо от библиотеки или фреймворка, который мы выберем для использования. Мир JavaScript разнообразен, и мы собираемся окунуться в него с головой. Пристегнитесь! Путешествие обещает стать еще более захватывающим. огромен и

ГЛАВА Альтернативы 1О React В предыдущей главе мы довольно подробно рассмотрели новую тему серверных компонентов React (React Server Components, RSC). Мы изучили, как они работают, когда их использовать и почему для них требуются мощные инструменты, такие как упаковщики нового поколения, маршрутизаторы и многое другое. Мы провели дальнейшее разделение между серверными компонентами и серверным рендерин­ rом и даже с нуля создали простой RSС-рендерер, чтобы понять лежащий в его ос­ нове механизм. Теперь, когда мы переходим к изучению альтернатив React, понимание роли и функций фреймворков и серверных компонентов обеспечит нам ценный контекст. Каждая библиотека, которую мы обсудим в этой главе, также поставляется со свя­ занными с ней фреймворками, и многие принципы и компромиссы, которые мы рассмотрели, изучая React, также применимы к этим экосистемам. В этой главе мы отвлечемся от React и его экосистемы и рассмотрим некоторые популярные альтернативные инструменты разработки пользовательских интерфей­ сов: Vue.js, Angular, Svelte, Solid и Каждая библиотека и ее фреймворк пред­ Qwik. ставляют собственную модель реактивности и подхода к разработке пользователь­ ского интерфейса. Понимание этих моделей и их особенностей может расширить наш кругозор и предоставить нам больше инструментов для решения проблем в наших проектах. Vue.js Vue.js - это популярный JavaScript-фpeймвopк для создания пользовательских ин­ терфейсов. Разработан Званом Ю (Evan You), бывшим инженером Google, который работал над проектами AngularJS, поэтому не удивительно, что Vue.js стремится использовать преимущества Angular, но в рамках более легкого, удобного в обслу­ живании и менее требовательного пакета. Одной из отличительных особенностей Vue является ненавязчивая система реак­ тивности. Состояние компонента складывается из реактивных объектов JavaScript. При их изменении представление обновляется. Это делает управление состоянием
328 Глава 10 простым и интуитивно понятным, однако важно понимать, как это работает, чтобы избежать некоторых распространенных ошибок. Особенность модели реактивности пись свойств объекта. Vue Vue 2 использовал в том, что он перехватывает чтение и за­ геттеры и сеттеры тельно из-за ограничений браузерной поддержки, но в (getter/setters) исключи­ Vue 3 для реактивных объ­ ектов используются прокси, а геттеры и сеттеры ~ для ссылок. Вот некоторый псевдокод из документации Vue, который иллюстрирует его работу: functi.on reactive(obj) { гetuгn new Ргоху(оЬj, { get(target, key) { track(target, key) гetuгn target[key] }, set(target, key, value) { target[key] = value trigger(target, key) } }) } functi.on ref(value) { const refObject = { get value() { track(refObject, 'value') гetuгn value }, set value(newValue) { value = newValue triggeг(refObject, 'value') } } гetuгn refObject } Здесь мы демонстрируем упрощенную реактивную систему, использующую про­ reactive принимает объект и возвращает прокси этого объек­ get и set. При выполнении операции get он функцию track и возвращает запрошенное свойство. При выполнении set он обновляет значение и вызывает функцию trigger. кси-серверы. Функция та, который перехватывает операции вызывает операции Функция геf, с другой стороны, инкапсулирует значение внутри объекта и обеспе­ чивает реактивные операции get и set для этого значения, аналогично прокси, но с
Альтернативы другой структурой, обеспечивающей надлежащий вызов функций React track 329 1 и tгigger в процессе доступа или модификации. Это очень простой пример реактивной системы, но он демонстрирует принципы модели реактивности Vue. Эту модель можно использовать даже для обновления DOM. Мы можем реализовать простой "реактивный рендеринг" следующим образом: l.Plpoгt { ref, watchEffect} const count fгor1 "vue"; = ref(0); watchEffect(() => { docul'lent.Ьody.tnnerHTML = 'count ts: ${count.value}'; }); // Обновление DOM count.value++; На самом деле, этот код довольно хорошо иллюстрирует, как компонент хронизирует состояние и Vue син­ каждый экземпляр компонента создает реактив­ DOM - ный эффект для рендеринга и обновления DOM. Vue исполь­ tnnerHTML, но этого Конечно, компоненты зуют гораздо более эффективные способы обновления DOM, чем должно быть достаточно, чтобы дать вам общее представление о том, как это работает. API ref(), COl'lputed() и watchEffect() являются частью АРI-композиции Vue. Сигналы Довольно много других фреймворков внедрили примитивы реактивности под на­ званием "сигналы", аналогичные ref из АРI-композиции Vue. Давайте их обсудим в этой главе. По сути, сигналы Vue. (signals) - это тот же тип примитивов реактивности, что и ref Это контейнер значений, который обеспечивает отслеживание зависимостей при доступе и запуск побочных эффектов при мутации. Эта парадигма, основанная на примитивах реактивности, не является особенно новой концепцией в мире ин­ терфейсов: она восходит к таким реализациям, как Кnockout observaЫes и Tracker, которые были выпущены более десяти лет назад. лиотека управления состоянием React Vue Options API Meteor и биб­ МоЬХ основаны на тех же принципах, но скрывают примитивы за свойствами объектов. Сегодня концепция сигналов часто обсуждается вместе с моделью рендеринга, в которой обновления выполняются с помощью детализированных подписок. Благо­ даря использованию виртуального ции Vue DOM, для достижения аналогичной оптимиза­ в настоящее время полагается на компиляторы. Однако разработчики также изучают новую стратегию компиляции, основанную на Solid (режим Vue Vapor),
330 Глава 10 которая не зависит от виртуального встроенной системы реактивности DOM и использует больше преимуществ Vue. Простота Главная сила Vue в его простоте. Начать работу с те просто включить библиотеку Vue невероятно просто: вы може­ в свой НТМL-файл с помощью тега <scгi.pt> и приступить к созданию компонентов также предоставляет инструмент Vue CLI, Vue. Для разработки новых проектов Vue который может стать отличным способом начать работу с более сложным приложением. Несмотря на то что здесь мы лишь поверхностно рассмотрели Vue.js, очевидно, что сочетание мощной системы реактивности, основанной на шаблонах синтаксиса и хорошо структурированной модели компонентов, делает Vue привлекательным ва­ риантом для многих разработчиков. Angular Angular, разработанный и поддерживаемый игроком в мире JаvаSсriрt-фреймворков. Google, Angular - является еще одним известным это законченный, уверенный в себе фреймворк, предоставляющий собственные решения для широкого спектра задач, связанных с интерфейсом, от рендеринга и управления состоянием до мар­ шрутизации и обработки форм. Angular внедряет модель реактивности, отличную от модели са определения и согласования виртуального известная как обнаружение изменений В Angular DOM в Angular (change detection). React. Вместо процес­ используется система, каждый компонент получает детектор изменений, отвечающий за про­ верку изменения компонента с помощью библиотеки под названием Zone.js. Преж­ де чем мы продолжим, давайте поговорим об этом немного подробнее. Обнаружение изменений Обнаружение изменений- это процесс, с помощью которого Angular проверяет, изменилось ли состояние вашего приложения и нуждается ли в обновлении DOM. Angular просматривает ваши компоненты сверху донизу в по­ Angular периодически запускает свой механизм обнаружения из­ На высоком уровне исках изменений. менений, чтобы изменения в модели данных отражались в представлении прило­ жения. Обнаружение изменений может быть запущено либо вручную, либо с помощью асинхронного события. Система обнаружения изменений оптимизирована и отличается высокой произво­ дительностью, но все равно может замедлять работу, если приложение запускает процесс обнаружения слишком часто. Эта система является мощным и гибким инструментом, а Angular предоставляет несколько готовых стратегий для тонкой
Альтернативы 331 React настройки ее поведения для оптимизации производительности в различных сце­ нариях. Angular также использует шаблонный синтаксис, как и Vue, но, кроме того, предос­ тавляет более мощные директивы и конструкции для управления *ngif для условного рендеринга элементов и отличает его от JSX React, *ngFor DOM, такие как для отображения списков. Это который для рендеринга динамических данных использует со встроенными выражениями JavaScript. Сигналы Angular претерпевает некоторые фундаментальные изменения, отказываясь от "грязной проверки" 1 и внедряя собственную реализацию примитива реактивности. Angular Signal API выглядит следующим const count = stgna1(0); count(); // досмуп к значению count.set(1); // усРЮновка нового значения count.update((v) => v + 1); // обновление, // образом: основанное на прежнем значении изменение глубоких обьекмов с одинаковой иденмичносмью const state = stgna1({ count: 0 }); state.~utate((o) => { o.count++; }); По сравнению с Vue ref, АРI-интерфейс Angular, основанный на геттерах, предос­ тавляет некоторые интересные компромиссы при использовании в компонентах ♦ О немного менее многословный, чем .va1ue, Vue: но обновление значения более под­ робное; ♦ геf-развертывания не существует: для доступа к значениям всегда требуется (). Это делает доступ к значениям везде единообразным. Это также означает, что вы можете передавать необработанные сигналы в качестве пропса компонента. Angular - это что-то вроде швейцарского армейского ножа, предоставляющего широкий спектр инструментов для создания сложных приложений. Его самоуве­ ренный характер может быть как сильной стороной в смысле структурного едино­ образия кодовой базы, так и ограничением с точки зрения гибкости и процесса обу­ чения разработчиков, начинающих работать с фреймворком. 1 Dirty-checking (в Angular) - регулярная проверка всех привязанных данных на предмет изменений. Angular обновляет представление соответствующим обра­ Если обнаруживаются любые изменения, зом. - Прим. пер.
332 Глава 10 Svelte Svelte - это радикально новый подход к созданию пользовательских интерфейсов. В отличие от традиционных фреймворков, Svelte - это компилятор, преобразую­ щий ваши декларативные компоненты в эффективный императивный код, который оперативно обновляет DOM. В результате вы можете создавать высокопроизводи­ тельные реактивные веб-приложения с меньшими объемами кода. Модель реактивности тивные операторы в Svelte невероятно проста, но в то же время эффективна. Реак­ Svelte написаны с использованием простого синтаксиса, напо­ минающего формулы для электронных таблиц. Вот основной Svеltе-компонент: <script> let count = 0; functi.on tncreмent() { count += 1; } </scгi.pt> <di.v>{count}</di.v> <button on:cltck={tncrefl1ent}> Cltck ме </button> В этом примере элемент разметки автоматически обновляться при каждом изменении переменной на чевым отличием: в Svelte {count} будет count. Это похоже JSX в React, но с одним клю­ эта реактивность реализуется автоматически. Вам не нужно вызывать сеттер-функцию или использовать какой-либо специальный для обновления DOM. Вы просто присваиваете переменной значение, и Svelte API по­ заботится обо всем остальном. Svelte также предлагает синтаксис реактивных инструкций, который позволяет вы­ числять значения на основе ваших реактивных данных: <SCript> let count = 0; let douЫeCount = 0; $: douЫeCount = count * 2; functi.on tncreмent() { count += 1; } </scгi.pt>
Альтернативы React 333 <diV>{douЫeCount}</diV> on:click={increrient}> Click 111е </button> <Ьutton В этом примере douЫeCount будет автоматически обновляться при каждом измене­ нии count. Это напоминает вычисляемые свойства в Vue, но синтаксис здесь проще. Подход к компиляции, используемый Svelte, правило, производительности это приводит к повышению имеет несколько преимуществ. Как поскольку отсутствует этап виртуального изменения ний. Вместо этого Svelte генерирует код, во который обновляет время выполнения, и внесения исправле­ DOM DOM напрямую. Однако этот подход также требует компромиссов. Ориентированный на компиля­ тор характер Svelte означает, что некоторые динамические возможности, предла­ гаемые виртуальными платформами на основе DOM, такие как динамические типы компонентов, могут быть более громоздкими или многословными. Кроме того, по­ скольку экосистема Svelte меньше и моложе, чем в случае React, Vue и Angular, в ней может быть меньше доступных ресурсов, библиотек и решений для сообщества. Руны Руны Svelte (runes) - это символы, которые влияют на компилятор сегодня использует let =, Svelte. В то время как $: для обозначения ключевое слово ехрогt и метку конкретных вещей, руны используют синтаксис функций для достижения тех же целей и даже большего. Например, чтобы объявить фрагмент реактивного состояния, мы можем использо­ вать руну $state: <script> + tet count = $state(0); function increrient() { count += 1; } </scгipt> <button on:click={increrient}> cHcks: {count} </button> По мере усложнения приложений определение того, какие значения являются реак­ тивными, а какие - нет, может стать сложной задачей. И текущая эвристика рабо-
Глава 334 10 тает только для деклараций let на верхнем уровне компонента, что может привести к путанице. Если код внутри файлов .svelte и js ведет себя по-разному, это может затруднить рефакторинг кода, например, если вам нужно превратить что-то в хра­ нилище, чтобы использовать в нескольких местах. С помощью рун реактивность выходит за рамки ваших файлов .svelte. Предполо­ жим, мы хотели инкапсулировать нашу логику счетчика таким образом, чтобы ее могли повторно использовать несколько компонентов. Сегодня вы бы сделали это с помощью пользовательского хранилища, разместив такой код в файле tripoгt { wri.taЫe js или .ts: } frOl'I "svelte/store"; functton createCounter() { const { subscгi.be, update} = wгi.taЫe(0); ехрогt { subscri.be, гetuгn i.ncreмent: () => update((n) => n + 1), }; } Поскольку это реализует технологию хранение контракта ние имеет метод подписки subscгi.Ьe - - возвращаемое значе­ мы можем ссылаться на значение в храни­ лище, добавляя к его названию префикс $: <scri.pt> + i.мрогt { cгeateCounteг} frOl'I './counteг.js'; + + const counteг = createCounteг(); let count = 0; functi.on i.ncгeмent() { count += 1; } </scri.pt> -<button on:cli.ck={i.ncreмent}> - cli.cks: {count} +<button on:cli.ck={counter.i.ncreмent}> + cli.cks: {$counter} </button> Это работает, но выглядит довольно странно! API хранилища может стать весьма громоздким, когда вы начнете делать более сложные вещи.
Альтернативы React 335 С рунами все становится намного проще: -tмрогt wгttaЫe} fгом { 'svelte/store'; function createCounter() { const { subscrtЬe, update} = let count = $state(0); ехрогt + wrttaЫe(0); return { subscгtЬe, i.ncгeмent: () => update((n) => n + 1) get count() { return count }, increмent: () => count += 1 + + }; } <Script> { createCounter} iмport fгом './counter.js'; const counter = createCounter(); </script> <button on:click={counter.increмent}> cltcks: {$counteг} + clicks: {counter.count} </button> Обратите внимание, что мы используем свойство что counter .count get в возвращаемом объекте, так всегда ссылается на текущее значение, а не на значение на момент вызова функции. Реактивность во время выполнения Сегодня Svelte использует реактивность во время компиляции . Это означает, что если у вас есть некоторый код, который использует метку $: для автоматического повторного запуска при изменении зависимостей, эти зависимости будут определе­ ны в процессе компиляции вашего компонента Svelte: <scri.pt> ехрогt ехрогt // // $: let width; let hei.ght; КомпиляГ'lор знаеf'I, чf'lo он должен выполниf'lь повГ'lорное вычисление ·агеа·. 'width • или 'height • меня/Оf'lся ... = width * height; Когда агеа
Глава 336 10 // должно регис~рирова~ься // Когда оно меняе~ся $: console.log(area); </scrtpt> значение ·агеа·. Это работает хорошо ... до тех пор, пока не перестает работать. Предположим, мы переработали код следующим образом: // @еггогs: 7006 2304 const мulttplyByНetght = (wt.dth) => wt.dth * hetght; $: агеа = мulttplyByHetght(wtdth); Поскольку в объявлении $: агеа = ... отображается только wtdth, оно не будет пе­ hetght. В результате код трудно поддается рефакторингу, тонкостях того, когда Svelte решает обновить те или иные значения, ресчитано при изменении и разобраться в может оказаться довольно непростой задачей, превышающей определенный уро­ вень сложности. В Svelte 5 представлены руны $dertved и $effect, которые в отличие от приведенно­ го выше примера определяют зависимости своих выражений при их вычислении: <scrtpt> let { wtdth, hetght} = $props(); // const агеа вмес~о ·ехрогt let' = $dertved(wt.dth * hetght); $effect( О => { console.log(area); }); </scrtpt> Как и в случае с лах $state, $deгlved и $effect также можно использовать в ваших фай­ .js и .ts. Усиление сигнала Как и в случае с любым другим фреймворком, в Svelte пришли к выводу, что Кnockout был прав с самого начала. Реактивность Svelte 5 основана на сигналах, которые, по сути, и были тем, что Кnockout делал в 2010 году. Совсем недавно сигналы были популяризированы Solid (подробнее об этом позже) и приняты множеством других фреймворков. В Svelte 5 сигналы являются скорее деталью внутренней реализации, а не тем, с чем вы взаи­ модействуете напрямую.
Альтернативы React 337 Solid это декларативная библиотека Solid - интерфейсов. Она похожа на понентную модель, но React JavaScript для создания пользовательских в том смысле, что предоставляет базовую ком­ Solid основана на реактивных Вместо использования виртуального DOM, Solid примитивах. применяет детализированную систему реактивности для автоматического отслеживания зависимостей и непо­ средственного обновления DOM, что может привести к более эффективным обнов­ лениям. Вот пример простого компонента 'U'lpoгt { createSi..gnal} fгOl'I Solid: "soli..d-js"; functton Coмponent() { const [count, setCount] = createSi..gnal(0); гetuгn ( <> <dtv>{count()}</dtv> <button onCli..ck={() => setCount(count() + 1)}>Increмent</button> </> ); } В этом примере React. createSi..gna l создает реактивный примитив, аналогичный Ключевое отличие заключается в том, что вращает текущее значение и неявно контекста. Когда вызывается функция count - регистрирует setCount, useState в это функция, которая воз­ зависимость для реактивного она запускает обновление для лю­ бой части пользовательского интерфейса, зависящей от count, без повторного вызо­ ва функциональных компонентов. В случае React, Coмponent был бы запущен повторно и выполнил бы всю логику внутри его блока. Таким образом, в React само значение count не является реактив­ Solid функция Coмponent никогда не активируется повторно, но само значе­ count является реактивным и изменяется при каждом вызове setCount. Это назы­ ным. В ние вается мелкозернистой реактивностью и прямо противоположно крупнозернистой реактивности React. Мелкозернистая система реактивности Solid позволяет свести к минимуму не­ нужные обновления и избежать необходимости в многочисленных этапах на­ стройки, что обеспечивает очень высокую производительность. Однако, посколь­ ку это относительно новая и менее используемая библиотека, в ней может быть не так много ресурсов и решений для сообщества, как в некоторых более известных платформах.
Глава 338 В интерфейсе 10 Solid API createSi..gnal() особое внимание уделяется разделению чте­ ния и записи. Сигналы представляются как геттер "только для чтения" и отдельный сеттер: const [count, setCount] = createSi..gnal(0); count(); // setCount(l); // дocf'lyn к значению обновление значения Обратите внимание, что сигнал count может передаваться без использования сетте­ ра. Это гарантирует, что состояние никогда не может быть изменено, если только сеттер не будет использован явно. оживил дискуссию вокруг сигналов, и, как мы видели ранее, эта концепция Solid была принята многими другими фреймворками и библиотеками. Все, что мы ранее упоминали о сигналах, взято из работы Райана Карниато Solid, (Ryan Camiato ), автора которому каким-то образом в одиночку удалось изменить всю интерфейсную экосистему, вернув концепцию 2010 года. Qwik Qwik - это уникальный фреймворк, предназначенный для оптимизации загрузки веб-страниц и определения приоритетов быстродействия и взаимодействия с поль­ зователем. В отличие от традиционных фреймворков, он рассматривает веб­ страницы как набор компонентов, которые можно независимо загружать по сети и взаимодействовать с ними по требованию. Такой подход значительно сокращает время начальной загрузки страницы, улучшая общий пользовательский опыт. Веб-приложения и сайты, созданные с помощью и постоянный начальный объем JavaScript (-1 Qwik, имеют чрезвычайно малый Кбайт). Объем исходного JavaScript, загружаемого сайтом является постоянным, поскольку он использует загруз­ чик известен в некоторых кругах как "платформа О( 1)", что Qwik. Вот Qwik, почему Qwik означает, что он имеет постоянное время загрузки независимо от размера приложе­ ния. Изначально Qwik загружает минимальное количество JavaScript, но затем подгру­ жает компоненты и другие функции по мере необходимости. Такой подход позво­ ляет Qwik нентов, устанавливать высокие приоритеты загрузкам наиболее важных компо­ что сокращает время начальной загрузки и повышает эффективность взаимодействия с пользователем. Важной особенностью Qwik является возможность возобновления работы. Мы в общих чертах рассмотрели концепцию возобновления работы в главе ной серверному React. Напомним, что возобновление работы - 6, посвящен­ это процесс, кото­ рый начинается с отправки отрисованного сервером снимка исходного состояния страницы клиенту. Когда пользователь открывает страницу, он взаимодействует с
Альтернативы React 1 339 этим статическим снимком до тех пор, пока ему не понадобится дополнительная интерактивность. Затем по мере продолжения работы загружаются различные до­ полнительные функции. Этот механизм предоставляет пользователю возможность получения мгновенного отклика, что не характерно для многих других фреймворков. Возобновляемость намного превосходит гидратацию (также рассмотренную в гла­ ве 6), поскольку она не требует повторного рендеринга компонентов. Это также позволяет избежать разочарования пользователя, когда веб-сайт не является инте­ рактивным в течение неопределенного периода времени после того, как первона­ чальная разметка, JavaScript загрузил При сравнении Vue, Svelte или отображаемая сервером, и обработал страницу. попала в Qwik же браузер, и до того, как включается мгновенно. Qwik с другими популярными фреймворками, такими Solid, можно выявить несколько отличий. React и Vue как React, также ис­ пользуют компонентный подход. Если мы не будем тщательно и намеренно фраг­ ментировать код, мы можем отправить клиенту такой пакет JavaScript, который иногда может достигать размера в несколько мегабайт. Этот процесс приведет к увеличению времени начальной загрузки, особенно для крупных приложений. С другой стороны, Qwik загружает компоненты и обработчики событий только по мере необходимости, что приводит к более стремительной начальной загрузке и более быстрому началу взаимодействия с пользователем. Qwik также хорошо раз­ бирается в предварительной выборке данных и делает это при помощи отложенной (ленивой) загрузки, так что все элементы предварительно выбираются при началь­ ной загрузке, но анализируются и выполняются в дальнейшем только по требованию. Qwik, как и Svelte и Solid, фокусируется на производительности, но достигает этого Svelte компилирует компоненты в высокоэффективный импера­ код, который непосредственно управляет DOM, в то время как Solid для иным способом. тивный своих компонентов использует мелкозернистую модель реактивности. Применяя также реактивные примитивы, Qwik фокусируется на оптимизации загрузки ком­ понентов и обеспечении того, чтобы наиболее важные из них стали доступны в первую очередь. Что касается уровня квалификации разработчиков, Qwik предлагает простой и ин­ API, который упрощает определение компонентов и работу с ними. Компоненты Qwik практически идентичны компонентам React с точки зре­ ния синтаксиса и структуры, поскольку они также выражаются с помощью JSX (или TSX). Это сходство облегчает разработчикам работу с Qwik, особенно если они уже знакомы с React. туитивно понятный Кроме того, Qwik поддерживает взаимодействие с кам переносить Rеасt-компоненты в React, что приложениях Qwik позволяет разработчи­ с помощью утилиты qwi.klfy. Такая совместимость является существенным преимуществом для разра­ ботчиков, которые хотят использовать Qwik, но также React. хотят воспользоваться бога­ той экосистемой библиотек и инструментов Qwik представляет собой новый подход к современной веб-разработке, основанный на компонентах и на управляемой событиями архитектуре. Ориентация на возоб-
340 Глава 10 новляемость работы и приоритетную загрузку отличает его от других фреймворков, таких как React, Vue, Svelte и Solid. Хотя каждый из этих инструментов имеет свои сильные стороны и варианты использования, уникальные возможности Qwik дела­ ют его захватывающим дополнением к ландшафту платформ веб-разработки. Этот фреймворк может быть правильным выбором для разработчиков, которые ищут производительный, ориентированный на пользователя и эффективный способ соз­ дания своих веб-приложений. Единственным недостатком Qwik является то, что он все еще довольно новый и не имеет такой развитой экосистемы, как React, Vue или Angular. Однако он набирает обороты и имеет растущее сообщество разработчиков и поклонников. Поскольку Qwik продолжает развиваться, будет интересно посмотреть, как он соотносится с другими фреймворками и как его можно использовать для создания еще более мощных приложений. Общие шаблоны Все эти технологии - React, Angular, Qwik, Solid и Svelte - являются решениями для создания богатых интерактивных пользовательских интерфейсов для Интерне­ та. Хотя они различаются по своей философии, методологиям и деталям реализа­ ции, у них есть несколько общих черт, которые отражают их общую цель. Архитектура, основанная на компонентах Одной из основных общих черт этих платформ и библиотек является использова­ ние архитектуры, основанной на компонентах. В такой архитектуре пользователь­ ские интерфейсы разбиты на отдельные части, или компоненты, каждый из кото­ рых отвечает за определенную часть пользовательского интерфейса. Компоненты инкапсулируют свое собственное состояние и логику, и их можно объединять для создания сложных пользовательских интерфейсов. Такая модуль­ ность способствует повторному использованию кода, разделению задач и улучше­ нию обслуживаемости кода. В каждой из этих платформ компоненты могут быть функциональными, и их часто можно компоновать, расширять или декорировать с целью создания более сложных компонентов. Декларативный синтаксис React, Angular, Qwik, Solid и Svelte для определения пользовательских интерфейсов используют декларативный синтаксис. При декларативном подходе разработчики определяют, как должен выглядеть пользовательский интерфейс для данного со­ стояния, а фреймворк заботится об обновлении пользовательского интерфейса в соответствии с этим состоянием. Это позволяет отказаться от императивных мани­ пуляций с DOM, которые могут сделать разработку пользовательского интерфейса утомительной и подверженной ошибкам.
Альтернативы React 1 341 Каждая из этих технологий предоставляет собственный язык шаблонов для написа­ ния декларативных пользовательских интерфейсов. JSX; Angular использует React, Qwik и Solid используют HTML, а Svelte имеет синтаксис шаблонов, основанный на собственный язык, в основе которого лежит HTML. Обновления Все эти библиотеки и фреймворки предоставляют механизм для реагирования на обновления состояния и соответствующего изменения пользовательского интер­ фейса. React и Vue ний в виртуальном для выполнения этих обновлений используют алгоритм измене­ DOM. Svelte, с другой стороны, компилирует компоненты в им­ перативный код, который напрямую обновляет DOM. Angular использует механизм обнаружения изменений в зонах и наблюдаемых объектах. Скорее всего, в дальнейшем React будет использовать vDOM, а все остальные­ различные сигналы. Несмотря на различные методы, цель у этих инструментов одна и та же: эффектив­ но обновить пользовательский интерфейс в ответ на изменения состояния, абстра­ гируя сложные манипуляции с DOM и позволяя разработчикам сосредоточиться на логике приложения. Методы жизненного цикла React, Angular, Solid и Svelte предоставляют методы или хуки, которые являются функциями, вызываемыми на разных этапах жизненного цикла компонента, напри­ мер при его первом создании, обновлении и удалении из DOM. Разработчики могут использовать эти методы для запуска побочных эффектов, очистки ресурсов или внесения обновлений на основе изменений в пропсах. Экосистема и инструменты Каждая из этих платформ поддерживается богатой экосистемой инструментов, библиотек и ресурсов. Все они работают с современными функциями и инструмен­ JavaScript, включая синтаксис ЕSб, модули и средства сборки, такие как Webpack и Babel. Они также имеют отличную поддержку TypeScript, что позволяет тами разработчикам писать типобезопасный код и использовать преимущества мощных функций TypeScript. Большинство из этих технологий также поставляются с продвинутыми инструмен­ тами для опытных разработчиков, которые могут помочь в отладке и профилирова­ нии приложений. Инструменты разработчиков React и Angular для популярных браузеров являются отличными примерами таких технологий. Хотя React, Angular, Qwik, Solid и Svelte обладают своими уникальными преиму­ ществами и философией, их объединяют следующие общие цели: организация ар­ хитектуры на основе компонентов, создания декларативных пользовательских ин­ терфейсов, реагирование на изменения состояния, упрощение обработки событий,
Глава 342 10 методы жизненного цикла или аналогичные концепции и поддержка богатой эко­ системы и современного инструментария JavaScript. Этот общий набор функций и концепций свидетельствует об эволюции веб-разработки в сторону более модуль­ ных, декларативных и реактивных парадигм. React не значит реактивный Термин "реактивный" используется для описания многих вещей в мире программи­ рования. Особенно часто он применяется для описания систем, которые автомати­ чески обновляются в ответ на изменения в данных. Парадигма реактивного про­ граммирования, по сути, заключается в создании систем, которые реагируют на изменения и автоматически распространяют эти изменения по системе. Вот почему такие фреймворки, как React Vue.js и Svelte, часто описываются как реактивные. Однако не следует традиционной модели реактивности, и его подход совершенно иной. React был представлен как библиотека для создания пользовательских интерфейсов декларативным способом пишет на React, - дек.шративность здесь означает, что те из нас, кто просто описывают, чего мы хотим, а React сам разбирается с тем, "как это сделать". Такой подход позволяет разработчикам описывать пользова­ тельский интерфейс на основе текущего состояния приложения, а React заботится об обновлении пользовательского интерфейса при каждом изменении состояния. Это описание может звучать так, будто React является реактивным, но когда вы уг­ лубляетесь в детали реализации, становится очевидным, что модель React сильно отличается от традиционной модели реактивного программирования. Для того чтобы понять, почему React не является реактивным в традиционном смысле, давайте сначала посмотрим, как выглядит традиционная реактивность. В традиционной реактивной системе зависимости между вычислениями автоматиче­ ски отслеживаются по мере выполнения вашего кода. Когда реактивная зависи­ мость изменяется, все вычисления, которые от нее зависят, автоматически выпол­ няются повторно, чтобы отразить это изменение. Обычно это делается с использованием таких методов, как привязка данных, отслеживание значений, сиг­ налов и временньrх интервалов. Сигналы, например, являются реактивным примитивом, который может использо­ ваться для создания реактивных значений: при чтении читатель, получающий сиг­ нал, подписывается на него, а при записи нового значения все подписчики получа­ ют уведомление. Это называется React reactivity 1О 1. использует другой подход для управления состоянием и его обновлениями. Вместо автоматического отслеживания зависимостей и распространения изменений React вводит более явный механизм обновления состояния - хук менении состояния вместо немедленного отображения обновлений useState. При из­ React планирует повторный рендеринг, и во время повторного рендеринга вся функция компонента запускается снова с новым состоянием.
Альтернативы React 1 343 Что это означает в случае такого счетчика? i.rlport React, { useState} frOl'I "react"; functi.on Counter() { const [count, setCount] = useState(0); functi..on tncre111ent() { setCount(count + 1); } гetuгn ( <di.v> <p>{count}</p> <button onCltck={tncre111ent}>Incre,,ient</Ьutton> </di.v> ); } ехрогt defautt Counter; Когда вызывается useState. setCount, функция Counter вызывается повторно, включая хук Это отличается от традиционной реактивной модели, где вместо повтор­ ного запуска всей функции обновляются только реактивные части пользователь­ ского интерфейса, в данном случае элемент {count} внутри <р>. Это называется крупнозернистой реактивностью и прямо противоположно модели сигналов с мел­ козернистой реактивностью. React часто V отождествляется со следующим выражением: = f(s) То есть представление является функцией своего состояния. Это выражение само по себе описывает нереактивный характер React: представление является функцией состояния, но оно не обновляется автоматически при изменении состояния. Вместо этого представление обновляется, когда функция повторно выполняется с новым состоянием. React начинается процесс определения DOM. Когда состояние компонента или Именно здесь в виртуального React повторно DOM. Затем он различий и согласования его пропсы изменяются, отображает компонент, создавая новое поддерево виртуального сравнивает это новое поддерево со старым, вычисляет минималь­ ный набор фактических мутаций DOM и применяет эти мутации к DOM. Эта модель явного задания состояния и повторной визуализации, в отличие от ав­ томатического реактивного распространения изменений, обеспечивает большую предсказуемость; React, если его очеловечить, сказал бы: "Расскажите мне, какое
344 Глава 10 состояние вы ожидаете, и я позабочусь об этом". Он включает такие функции, как пакетное обновление состояния, и упрощает получение представления о состоянии приложения в любой момент времени, поскольку обновление состояния и резуль­ тирующее обновление пользовательского интерфейса связаны в рамках одной ато­ марной операции. Однако это также означает, что компоненты React менее реактивны в традицион­ ном смысле этого слова. Они не реагируют автоматически на изменения в данных. Вместо этого они явно описывают, как должен выглядеть пользовательский интер­ фейс для данного состояния, и React дает команду на применение любых необхо­ димых обновлений при изменении состояния путем повторного выполнения функ­ ций, а не просто обновляет соответствующие значения на месте. Хотя подход React не является реактивным в смысле автоматического отслежива­ ния и распространения изменений, он по-прежнему обеспечивает высокоэффектив­ ный механизм для создания динамичных, интерактивных пользовательских интер­ фейсов. Использование state и ргорs для управления отображением обеспечивает четкую и предсказуемую модель для понимания того, няются по приложению, а виртуальная система лениями реального как изменения распростра­ эффективно управляет обнов­ DOM DOM. В конечном счете вопрос о том, считать ли подход React реактивным, сводится к семантике. Если вы определяете реактивность как автоматическое распространение изменений по системе, то нет, React не является реактивным. Но если вы опреде­ ляете реактивность как способность системы реагировать на изменения состояния предсказуемым и контролируемым образом, то да, React, безусловно, можно счи­ тать реактивным. При изучении React и других фреймворков и библиотек становится ясно, что при разработке пользовательского интерфейса не существует универсального подхода к управлению состоянием и реактивностью. Каждый инструмент имеет свои сильные стороны и недостатки и подходит для разных вариантов использования. Понимание различий имеет решающее значение при выборе подходящего инструмента для ра­ боты, а также может помочь в написании более эффективного кода, независимо от используемой платформы или библиотеки. Модель обработки состояния и обновлений в React обеспечивает превосходный баланс между контролем и удобством работы. Механизм явного обновления со­ стояния позволяет разработчикам упростить анализ состояния приложения, в то время как алгоритм согласования и определения различий эффективно применяет обновления к DOM. Несмотря на то что подход React не является "реактивным" в традиционном смысле этого слова, он оказался невероятно эффективным для соз­ дания сложных пользовательских интерфейсов. Нельзя отрицать, что модели реактйвного программирования обладают неоспори­ мыми преимуществами, особенно когда речь идет об автоматическом управлении зависимостями и обновлениями. Но, как мы уже видели, подход React обладает ря­ дом собственных достоинств, обеспечивая высокую степень контроля и предска­ зуемости.
Альтернативы React 345 Для апологетов "завершенности", мы рассмотрим, как тот же счетчик будет выгля­ деть в 'U'lpoгt Solid, платформе, использующей { createSi.gnal} fгOl'I "soli.d"; реактивную модель: functi.on Counter() { const [count, setCount] = createSi.gnal(0); functi.on i.ncref"lent() { setCount(count + 1); } гeturn ( <di.v> <p>{count()}</p> <button onCli.ck={i.ncre111ent()}>Incre111ent</button> </d'l.v> ); } ехрогt default Counter; В этом примере count является реактивным свойством данных компонента. Когда мы впервые считываем count, count() внутри наших элементов JSX на реактивное значение count. вызывая явно подписываем эту часть нашего Затем, когда мы вызываем функцию setCount, setCount i.ncre111ent( ), <р>, мы не­ которая в свою очередь вызывает обновляет значение и уведомляет всех подписчиков об изменении значения, предлагая им обновить его. Это немного похоже на шаблон puЪ/sub, ко­ гда подписчик подписывается на издателя, а издатель рассылает уведомления всем подписчикам. Результатом является мелкозернистая реактивность: сам функциональный компо­ нент Counter никогда не вызывается более одного раза, но детализированные, не­ большие реактивные значения обновляются. Пример: зависимые значения Рассмотрим компонент, который отображает список элементов и их количество. В реактивной системе, такой как Svelte, ляться при каждом изменении списка: <scri.pt> let i.teмs = ['Apple', 'Banana', 'Cherry']; $: count = i.teмs.length; </scri.pt> количество будет автоматически обнов­
Глава 346 10 <p>{count} tteмs:</p> <Ul> {#each iteмs as tteм (tteм)} <Н>{Нем}</Н> {/each} </Ul> Здесь $: count = Heмs. length; объявляет реактивную инструкцию. Всякий раз, ко­ гда элементы Немs меняются, общее количество count автоматически пересчитыва­ ется. В React это выглядит немного по-другому: Vlport React, { useState} fгor1 "геасt"; functi.on Iteмlist() { const [Немs, setiteмs] = useState(["Apple", "Banana", const count = iteмs.length; // ... где-мо происходим обновление iteмs гetuгn ( "Сhеггу"]); <di.v> <p>{count} iteмs:</p> <U\> {tteмs.мap((iteм) <\'\. ))} => ( key={iteм}>{iteм}</\i.> </u\> </di.v> ); } ехрогt defau\t Iteмlist; В этом компоненте React значение count не является реактивным значением, кото­ рое автоматически обновляется при изменении Немs. Здесь это значение, получен­ ное из текущего состояния на этапе рендеринга. Когда Немs изменяется, нам нужно вызвать setiteмs, чтобы обновить состояние и инициировать повторный рендеринг, в котором count пересчитывается не потому, что count компонент функции Iteмlist активирован повторно. "реагирует", а потому, что
Альтернативы Будущее React 347 React Учитывая широкое распространение реактивных примитивов, таких как сигналы, во всей интерфейсной экосистеме, некоторые могут предположить, что React нечном итоге примет аналогичный ("сигнальный") подход. Однако команда в ко­ React заявила, что они "не в восторге" от сигналов, и выбирает альтернативную техноло­ гию, чтобы добиться аналогичных преимуществ в производительности, которые обеспечивают сигналы. Для того чтобы лучше понять это, давайте подытожим некоторые вещи, о которых React, на примере. Рассмотрим следующий компонент: { useState} frOl'1 "геасt"; React, il'lport il'lport { Col'lponentWi.thExpensi..veChHdren} frOl'1 "./Expensi..veCoмponent"; мы узнали о functi.on Counter() { const [count, setCount] = use5tate(0); functi.on i..ncreмent() { setCount(count + 1); } гetuгn ( <di.v> <p>{count}</p> <button onCli..ck={i..ncreмent}>Increмent</button> <C01'1ponentWt.thExpensi.veChi.ldгen /> </di.v> ); } ехрогt default Counter; В этом очень, очень замысловатом примере у нас есть компонент, содержащий со­ стояние с именем Counter с несколькими дочерними элементами: ♦ элемент <р>, который отображает текущее значение счетчика; ♦ элемент ♦ компонент <CoмponentWi.. thExpensi..veChi..ldren>, который отображает некоторые до­ <button>, который увеличивает значение счетчика; рогостоящие дочерние элементы с большим количеством вычислений. Теперь предположим, что мы нажимаем кнопку, чтобы увеличить ходит'? Функция Counter count. Что проис­ вызывается (повторно запускается, отрисовывается заново)
Глава 348 10 вместе со всеми ее дочерними элементами. Это поведение Оно означает, что компонент React по умолчанию. <COl'lponentWi.thExpensi.veChi.ldren> отрисовывается по­ вторно, хотя в этом нет необходимости: его пропсы или состояние не изменились! Такая грубая реактивность делает React менее производительным, чем он мог бы быть. Однако это довольно легко исправить: мы просто включаем меl'Ю в нужное время и в нужном месте: i.rlpoгt '\.Pll)Oгt React, { useState, меl'Ю} fгOl'I "react"; { COl'lponentWi.thExpensi.veChi.ldren} fгOl'I "./COl'lponentWi.thExpensi.veChi.ldren"; functton Counter() { const [count, setCount] = useState(0); functi.on i.ncreмent() { setCount(count + 1); } ( <di.v> <p>{count}</p> <button onCli.ck={i.ncreмent}>Increмent</button> гetuгn <Нerloi.zedCOl'lponentWi.thExpensi.veChi.tdгen /> </di.v> ); } const Мel'Юi.zedCoмponentWi.thExpensi.veChi.ldren = f11ef1IO( CoмponentWi.thExpensi.veChi.ldren ); ехрогt defautt Counter; Это работает до тех пор, пока мы не забываем использовать меl'Ю везде, где нам нужно. Действительно, в этом случае обеспечивается такая же детализированная реактивность, как и при использовании сигналов. Однако это не так удобно, как сигналы, потому что мы должны помнить, что меl'Ю требуется использовать везде, где нам необходимо. Многие из нас на данный момент, возможно, думают, что сигналы могли бы легко решить эту проблему, но команда React в Meta считает, что сигналы, как и меl'Ю, мо­ гут быть деталями реализации, о которых обычным разработчикам, использующим React, не следует задумываться. Они прислушиваются к первоначальному ценност-
Альтернативы 349 React React: "Описывайте свой пользовательский интерфейс деклара­ React сделать все остальное". Команда React считает, что лучший ному предложению тивно и дайте способ - это когда разработчики не заботятся о сигналах, 111еГГ10 или каких-либо де­ талях, а фреймворк должен быть в состоянии найти оптимальный способ визуали­ зации пользовательского интерфейса. С этой целью команда работает над новым программным обеспечением, предна­ значенным именно для этого. Речь идет о React Forget. React Forget Forget - это набор инструментов для React, похожий на линтер 2 (linter) с включен­ ным флагом код React меняться - -fi.x. Он применяет правила а затем автоматически преобразует React, в оптимальный, интеллектуально запоминая значения, которые не будут на протяжении всего жизненного цикла приложения, такие как COГ'lponentWithExpensiveChildren. React Благодаря правилам компилятор Forget может предсказать эти значения и сохранить их в памяти для нас. Это аналогичный подход, который использует но вместо компиляции в императивный код тельный код Forget Svelte, компилирует в более производи­ React. Каковы эти правила React? Давайте их перечислим: являются чистыми функциями. 1. Предполагается, что компоненты 2. Некоторые хуки и пользовательские обработчики событий не обязательно долж­ React ны быть чистыми. 3. 4. 5. Запрещенные действия в чистых функциях включают в себя: • изменение переменных/объектов, которые не были созданы в функции заново; • чтение свойств, которые могут измениться. Разрешенные действия включают: • чтение свойств или состояния; • генерирование ошибок; • мутация вновь созданных объектов/привязок. Отложенная (ленивая) инициализация - это исключение, допускающее мута­ цию с целью инициализации. 6. Объекты или замыкания, созданные во время рендеринга, не должны изменяться после завершения рендеринга, за исключением изменяемых объектов, сохранен­ ных в состоянии. 2 Линтер (от англ. lint) - инструмент программирования, который используется для анализа исход­ ного кода программного обеспечения с целью выявления потенциальных проблем, структурных оши­ бок, стилевых нарушений и других недо•1етов. - Пр11.11. ред.
350 Глава 10 Благодаря этим правилам компилятор Forget может предсказать, какие значения не будут меняться на протяжении всего жизненного цикла приложения, и сохранить их в памяти для нас. Что в результате? Высокооптимизированный, высокопроизво­ дительный код который конкурирует по производительности с другими биб­ React, лиотеками, использующими сигналы. На момент написания этой книги Forget находится на стадии оценки в Meta и пре­ восходит ожидания по использованию в крытого исходного кода, но команда Instagram и WhatsApp. У него еще нет от­ React рассматривает возможность его выпуска в качестве программного обеспечения с открытым исходным кодом в ближайшем будущем. Forget против сиrналов Поскольку Forget еще не имеет открытого исходного кода, довольно сложно сколько­ нибудь авторитетно комментировать его компромиссы. Однако мы можем предпо­ ложить, что если Forget действительно запоминает все, что не меняется, то мелко­ зернистая реактивность сигналов все равно может превосходить крупнозернистую реактивность, достигаемую с помощью React Forget, потому что сигналы живут в параллельной вселенной за пределами иерархии компонентов. Таким образом, когда произойдет обновление, React все равно придется пройти по всему дереву компонентов и сравнить новые и старые значения пропсов каждого компонента, чтобы определить, какой из них нуждается в повторном рендеринге. По-другому происходит в "сигнальных" фреймворках, где обновляются только реа­ гирующие части пользовательского интерфейса без необходимости обхода дерева. Эти предварительные данные позволяют предположить, что даже при использова­ нии компилятора Forget React все еще может работать медленнее, чем библиотеки, в которых по умолчанию используются сигналы, но пока рано говорить об этом. Обзор главь1 Эта глава началась с краткого резюме главы использование RSCs. Затем мы JаvаSсriрt-фреймворков, таких как в которой мы подробно рассмотрели 9, изучили обширный спектр Angular, Vue, Sve\te, Solid и альтернативных Qwik, стремясь по­ нять различия и сходства между этими библиотеками и фреймворками. Мы начали с рассмотрения Vue.js и изучили, как он использует декларативный подход к созданию пользовательских интерфейсов и способствует четкому разде­ лению задач благодаря своей архитектуре, основанной на компонентах. Затем мы погрузились в Angular, Svelte, Solid и Qwik, исследуя их философию и уникальные особенности. Мы рассмотрели, как они используют реактивные при­ митивы для автоматического обновления пользовательского интерфейса в ответ на изменения в данных и чем они отличаются от React в этом отношении.
Альтернативы React 351 После индивидуального изучения мы сравнили эти библиотеки пользовательского интерфейса между собой, подчеркнув их сильные и слабые стороны и области сов­ падения. Мы рассмотрели их модели реактивности, архитектурные решения, опыт разработки и характеристики производительности. С помощью примеров кода мы продемонстрировали уникальные качества каждой библиотеки, что помогло нам лучше понять их различия. Мы также рассмотрели концепцию реактивности и каким образом она реализуется в различных библиотеках. Мы обсудили, почему React не является реактивным в традиционном смысле этого слова, и выяснили, что он использует более грубый подход, при изменение состояния котором приводит к повторному рендерингу, в отличие от детализированной модели реактивности, которую можно найти в таких библиотеках, как Vue или Svelte. Наконец, мы задумались о будущем React и о том, в каком направлении он может развиваться в ближайшие годы. Мы обсудили подход команды React к реактивно­ сти и чем он отличается от традиционной модели реактивного программирования. Мы также познакомились с компилятором бор инструментов для React, Forget, который представляет собой на­ автоматически оптимизирующий код React. Оптими­ зация включает в себя запоминание 3начений, которые не меняются на протяжении всего жизненного цикла приложения. Давайте, наконец, посадим наш самолет. Проверьте ваши знания Вот список вопросов, которые помогут вам разобраться в концепциях, рассмотрен­ ных в этой главе. Если вы сможете уверенно ответить на все вопросы, отлично! Это признак того, что вы получаете знания из этой книги. Если у вас возникнут про­ блемы, возможно, стоит перечитать эту главу. 1. Чем отличается модель реактивности в React, Vue, Svelte, So\id и Angular? Как эти различия влияют на производительность и опыт разработки этих библиотек/ фреймворков? 2. Опишите уникальный подход Qwik к максимизации производительности. Как он отличается от подхода других библиотек пользовательского интерфейса, кото­ рые мы обсуждали'? 3. Каковы сильные и слабые стороны каждой библиотеки пользовательского ин­ терфейса, рассмотренной в этой главе? Как эти сильные и слабые стороны могут повлиять на выбор библиотеки/фреймворка для конкретного проекта? 4. React не является реактивным в традиционном смысле этого слова. Объясните это утверждение подробнее, сравнив его с моделью реактивности на основе push, 5. которую можно найти в таких бибJJиотеках, как Что такое React Forget? Vue или Svelte. Как он работает? Как он соотносится с сигналами?
352 Глава 10 Что дальше? По мере того, как мы приближаемся к завершению этого всестороннего путешест­ вия по миру React и его экосистеме, мы готовимся обобщить все, что нам удалось узнать. В следующей, заключительной, главе мы сделаем шаг назад и проанализи­ руем весь ландшафт в целом. Мы завершим работу над этой книгой и дадим целостное представление о том, где мы находимся сегодня и чего можем ожидать завтра. При этом мы будем опираться на все технические знания и понятия, которые мы почерпнули из этой книги. Мы прошли большой путь от понимания внутренней работы Rеасt-согласователя и погружения в асинхронность до работы с серверными компонентами, осмысления различных фреймворков React и сравнения React с аналогичными системами ~ все это было сделано с определенной целью. Теперь мы готовы сопоставить все факты, увидеть картину в целом и наметить дальнейший путь. Итак, вы готовы совершить прыжок в будущее Не пропустите грандиозный финал! React и фронтенд-разработки?
ГЛАВА 11 Заключение Если вы зашли так далеко, самое время поблагодарить вас за то, что присоедини­ лись ко мне в этом путешествии по экосистеме React. Надеюсь, вам понравилось это приключение так же, как и мне. За время, проведенное вместе, мы стремились глубже понять React, изучая его основные принципы, внутреннюю работу и его широкую экосистему. Предполагая, что мы уже знаем, как использовать React, мы - с сосредоточились на понимании его механизма: как он работает на самом деле конечной целью получить практические рекомендации, которые мы сможем ис­ пользовать в нашей инженерной карьере в будущем. Итоговые выводы Давайте сейчас очертим некоторые из этих выводов. Переосмысление лучших практик. Бывают ситуации, когда нам нужно все переосмыслить. Внедрение виртуального DOM React JSX и стало радикальным изменением существующего положения вещей. Это бросило вызов устоявшимся соглашениям и заставило нас переос­ мыслить то, как мы создаем интерфейсы. Эта готовность оспорить статус-кво и пересмотреть то, как все делается, является отличительной чертой философии React. Следовательно, как инженеры, мы всегда должны быть готовы бросить вызов существующему положению вещей и переосмыслить подходы и концепции. Полное понимание, как работает JSX - например, если мы не можем JavaScript - у нас, инженеров, есть возможность изменить это, создав новый язык. Это полностью относится к JSX, сравнительно новому языку, который компилируется в JavaScript. Мы то же са­ мое можем сделать теперь, когда полностью понимаем, как работает JSX, и не­ Если мы ограничены языком программирования использовать синтаксис в стиле HTML в много разбираемся в теории компиляторов. Ограничения ~ это не так уж плохо. Ограничения - это мать изобретений. React - денная ограничениями Интернета, где чтение это, по сути, инновация, порож­ i.nnerWi.dth элемента приводит к
Глава 354 11 переформатированию, и где разные браузеры используют разные API для собы­ тий. Вывод из этого заключается в том, что ограничения ~ это не так уж плохо. Они заставляют нас мыслить нестандартно и находить креативные решения. Декларативные абстракции открывают широкие воз,чожности. Отделив выражение JSX от средства согласования, React к разработке пользова­ тельского интерфейса впервые применил подход "напиши один раз, запусти в любом месте", позволяющий нам использовать один и тот же код для рендерин­ га в DOM, на сервере или даже на нативной платформе. Это мощная возмож­ ность, которую мы можем применить в наших собственных проектах, поскольку стараемся разделить проблемы и достичь нужного уровня абстракции. Раскрытие ,wощных возможностей позволяет нам создавать более гибкие и удоб­ ные в обслуживании приложения. Мы обнаружили ряд шаблонов, от компонентов более высокого порядка для ви­ зуализации пропсов до хуков для контекста. Эти шаблоны являются мощными инструментами, которые мы можем использовать для абстрагирования логики, распределения поведения между компонентами и более эффективного управле­ ния состоянием. Хотя эти шаблоны создают сложности, они также открывают мощные возможности, позволяя нам создавать более гибкие и удобные в обслу­ живании приложения. Более того, как и HOCs 1, эти шаблоны появились раньше React. Какие из шаблонов, которые мы используем сегодня, станут основой сле­ дующего поколения фреймворков пользовательского интерфейса? Какие шаб­ лоны мы можем изобрести, чтобы облегчить нашу жизнь? Мощ11ые возмо:жности могут быть исполыова11ы в 1шших собстве1111ых проектах. Мы узнали, что когда мы выходим за рамки браузера и обращаемся к серверу, открывается множество новых возможностей. Мы можем визуализировать наши компоненты React на сервере, использовать брау·1ерный API извлече11ия данных и применять собственные НТМL-формы для ввода да11ных поль·ювателем. Это мощные возможности, которые мы можем исполь·ювать в нашей деятельности 110 созданию собственных проектов, иоскольку мы работаем в и11тересной об­ ласти творческих компромиссов между рендерингом на сторо11е сервера и 11ре­ имуществами исполь·ювания фундаментальных веб-тех11шюгий. Преимущества улучшения полыователы·ко.'о опыта. Теперь у нас есть во'3можность для улучшения взаимодействия с 110;1ьзователя­ ми в полной useTrans1.t'\.on, мере применять конкурентные функции React, такие как откладывая работу, которая должна быть выr10ш1е1ш "в альтерна­ тивной вселенной", а затем фиксируя изменс1шя в DOM, ког;tа они будут ппо­ вы. Это мощ11ый потенциал, который мы можем ис1ю;1ь·ювать в наших собст1 Компонент n1,11.:111c10 Iюr1щка (11igl1cr-ordcr cшпroncnt, НОС) олин и·1 11rоюн111у11,1х 1.:1ю1.:обо11 ;1;Iя 110втор1ю1·0 ИCIIOJll,IOllallHH 110111КИ. нос IIC ЯRЛЯIОТСЯ 'll\CTl,IO АР! Rcact, 110 1 1.100 11r11мс11яюн·я IП•'НI КОМ110ЗИЦИО1111ОЙ 11рирою,I KOMIIOIICHTOR. -- ПрtlМ. IU!f),
Заключение 1 355 венных проектах, учитывая компромиссы между отсрочкой выполнения и пре­ имуществами улучшения пользовательского опыта. Все это делается на языке, который мы знаем и понимаем. Мы изучили тонкости взаимодействия Next.js и Remix через призму создания нашего собственного фреймворка и в конечном итоге пришли к выводу, что все это всего лишь JavaScript с использованием сервера. Мы всегда можем создать собственный фреймворк, если у нас будет достаточно времени и ресурсов. Мы благодарны авторам фреймворков за их работу и с удовлетворением понимаем, что все это сделано на языке, который мы знаем и умеем использовать. Отправляйте пользователям меньше кода. Как и при переходе от браузера к серверу, мы узнали, что переход от браузера к модульному упаковщику открывает совершенно новый мир возможностей, по­ зволяя отделять клиентские компоненты от серверных и предоставлять нашим пользователям значительно меньше кода. Какие еще интересные приемы компи­ ляции/компоновки мы можем использовать, чтобы улучшить работу наших пользователей? Мы можем черпать вдохновение в других фреймворках и применять их преимуще­ ства в наших собственных проектах. Мы уменьшили масштаб и посмотрели, каким образом за пределами React ре­ шается все та же проблема: как создавать пользовательские интерфейсы, кото­ рые были бы быстрыми, отзывчивыми, реактивными, а также обеспечивали бы отличный опыт для разработчиков? Мы изучили некоторые идеи из Qwik и других Vue, Solid, платформ и поняли, что можем вдохновляться решениями других фреймворков и применять их в наших собственных проектах. Подводя итоги нашего исследования React.js, важно поразмыслить о пройденном нами пути и понять преобразующую природу этой библиотеки. Многолетний рост React свидетельствует о ее адаптивности, жизнестойкости и инновационном духе сообщества. Пройдя путь от внедрения более интуитивно понятного способа со·ща­ ния интерфейсов с помощью JSX до переосмысления того, как обновления могут стать более эффективными с помощью виртуального DOM, React, несомненно, ос­ тавил и сохраняет неизгладимый след в мире веб-разработки. Этапы нашего пути В первых главах этой книги представлено краткое введение в основные принципы React. В основе философии React лежит создание компонентов, которые делают обновления веб-приложений более доступными, масштабируемыми и поддержи­ ваемыми. Автономные рабочие единицы элементы - React - компоненты, волокна (fibers), инкапсулируют как логику, так и пользовательский интерфейс, упро­ щая работу с нашими приложениями по мере их масштабирования.
Глава 356 React 11 с помощью JSX предлагает декларативный подход к разработке пользова­ тельского интерфейса. Делая интерфейсы функцией состояния нашего приложения, мы можем легко понять и предсказать, как изменения в наших данных повлияют на пользовательский интерфейс. Четкое разделение и концепция "единого источника истины", несомненно, изменили подход разработчиков к созданию пользователь­ ского интерфейса. По мере того как React набирал популярность, его влияние распространялось и на технологическую индустрию, вдохновляя множество платформ и фреймворков. Одной из наиболее заметных платформ, оказавших влияние на SwiftUI, фреймворк вах Apple. Под влиянием софии. React Вместо Controller), и других фреймворков классического шаблона SwiftUI является придерживается схожей фило­ проектирования который часто используется при разработке на разработчиков Apple, для создания пользовательских интерфейсов во всех устройст­ создавать пользовательские интерфейсы, MVC (Model-ViewiOS, SwiftUI поощряет применяя структуры меньшего размера, похожие на компоненты, называемые представлениями Каждое представление в жей на компоненты SwiftUI React. (views). является автономной единицей, во многом похо­ По мере развития фреймворков пользовательского интерфейса взаимное опыление идеями будет продолжаться. Инновации для одной платформы могут вдохновлять на улучшения в другой, что приводит к более богатому интерфейсу и более насы­ щенному ландшафту развития. Влияние React на и экосистему в целом яв­ SwiftUI ляется ярким примером таких симбиотических отношений и создает основу для будущего сотрудничества и вдохновения в мире технологий. Механика, лежащая в основе волшебства Виртуальный DOM и FiЬеr-согласователь были одними из наиболее технических тем, которые мы изучали. Эти концепции - шестеренки механизма, лежащего в основе эффективных и производительных обновлений React. Виртуальный действует как посредник между состоянием нашего приложения и реальным Сравнивая различия и пакетно обновляя данные, React DOM DOM. гарантирует, что для син­ хронизации пользовательского интерфейса с состоянием приложения выполняется минимум работы. FiЬеr-согласователь, с другой стороны, является мозгом, стоящим за этой операцией. Он решает, когда и как обновлять компоненты, оптимизируя производительность и обеспечивая согласованность. Мы изучили внутреннюю работу согласователя, уз­ нали о различных этапах его работы. Мы также рассмотрели, как согласователь оп­ ределяет приоритеты в работе, гарантируя, что наиболее важные обновления обра­ батываются в первую очередь.
Заключение 357 Расширенные возможности Углубляясь в неизведанные территории, мы рассмотрели расширенные шаблоны React. Эти шаблоны, такие как компоненты более высокого порядка (HOCs), про­ псы рендеринга, хуки и контекст, позволяют разработчикам абстрагировать логику, распределять поведение между компонентами и более эффективно управлять со­ стоянием. Хотя эти шаблоны и усложняют работу, они также открывают широкие возможности, позволяющие нам создавать более гибкие и удобные в обслуживании приложения. React помогли нам проследить эволюцию при­ Серверный и конкурентный ложений связи с растущей потребностью в быстрой начальной загрузке и в React React. В мгновенном начале интерактивного взаимодействия использование сервера и асин­ хронных операций стало жизненно важным. Эти методы позволяют нашим прило­ жениям оставаться быстрыми, отзывчивыми и ориентированными на пользователя. Мы изучили серверную часть react-dOl'I, включая такие функции, как renderToStгlng и renderToPi.peaЫeStreal'l, описав их компромиссы. Мы также рассмотрели некоторые асинхронные возможности React, такие как useSyncExternalStore и useTransi.ti.on, и способы их использования для улучшения взаимодействия с пользователем. Наконец, мы познакомились с серверными компонентами полнением к экосистеме React, React, более поздним до­ которое продолжает непрерывную эволюцию биб­ лиотеки. Осуществляя рендеринг компонентов только на сервере, мы можем созда­ вать более эффективные приложения, оптимизируя как производительность, так и удобство работы с пользователем. В наших заключительных главах с помощью альтернативных фреймворков и биб­ лиотек мы рассмотрели более широкую экосистему, окружающую React React. Успех породил множество инструментов, фреймворков и библиотек, имеющих свой набор преимуществ и компромиссов. React прошел долгий путь с момента своего создания, и его развитие является от­ ражением постоянно развивающегося мира веб-разработки. Читая эту книгу, вы не только узнали о библиотеке, но и получили представление о парадигмах и принци­ пах, лежащих в основе современной веб-разработки. Будьте в курсе последних событий Идти в ногу с постоянно развивающейся экосистемой множество фреймворков, созданных на базе React, JavaScript, включающей может показаться непростой задачей. Каждый год появляется множество новых инструментов и библиотек, обладающих своим набором функций, преимуществ и компромиссов. Для разра­ ботчика, принимающего обоснованное решение о том, какую платформу исполь­ зовать для будущего проекта, требуется нечто большее, чем просто знакомство с
Глава 358 11 текущим состоянием экосистемы. Это также требует понимания перспектив раз­ вития этих инструментов и того, как они вписываются в более широкий контекст веб-разработки. Существует несколько стратегий, позволяющих быть в курсе последних событий и принимать обоснованные решения о выборе подходящего фреймворка React для ваших будущих проектов. Следите за надежньши источниками. Экосистема JavaScript развивается быстрыми темпами. Важно следить за надеж­ ными источниками, которые предоставляют качественный контент и регулярные обновления о последних тенденциях и инструментах. Это могут быть блоги, ка­ налы YouTube, информационные бюллетени, подкасты или онлайн-сообщества. Например, ознакомление с официальными благами и аккаунтами компаний Next.js и Remix в Twitter может дать представление об их перспективных функ­ циях, улучшениях и общем плане развития. Мы рекомендуем использовать следующие источники: • • документы React на react.dev (https://react.dev/); ключевые члены команды React в Ж, ранее Twitter. Список может быть рас­ ширен: • 0 @sophieblts; 0 @sebmarkbage; 0 @zmofei; 0 @acdlite; 0 @rickhanlonii; 0 @dan_abramov2; создатели сообщества 0 @kadikraman; 0 @kentcdodds; 0 @shaundai; 0 @Saurav_ Varma; 0 @rachelnabors. React в Ж, включая, но не ограничиваясь: Присоединяйтесь к соответствующи.н сообщества.н. Онлайн-сообщества, такие как группы Discord и Slack, Reddit, Stack Overflow, GitHub или различные являются отличными местами для отслеживания новых тенденций и инструментов. Члены сообщества часто делятся своим опытом ра­ боты с фреймворками, что может дать полезную информацию при выборе меж­ ду различными инструментами.
Заключение 1 359 Вот некоторые полезные ресурсы сообщества: • The React subreddit; • The Reactiflux Discord server; • The bytes.dev newsletter; • The React Roundup podcast; • The "This Week in React" newsletter. Посещайте конференции и встречи. Конференции и встречи (meetups) отлично подходят для того, чтобы их участ­ ники оставались в курсе последних разработок и лучших практик в экосистеме JavaScript и React. Даже если вы не можете присутствовать лично, на многих из этих мероприятий предлагается онлайн-трансляция или запись выступлений для последующего просмотра. Вот несколько замечательных конференций React, на которых стоит побывать: • React Brussels; • React Alicante; • React lndia; • React Day Verona. Поэкспериментируйте с различными фреймворками. Когда дело касается понимания инструмента, ничто не сравнится с практиче­ ским опытом. Выделение некоторого времени на создание небольших проектов или прототипов с использованием различных фреймворков может дать бесцен­ ную информацию. Это поможет вам понять сильные и слабые стороны каждого фреймворка и ответит на вопрос, насколько они соответствуют вашему стилю разработки и требованиям проекта. Созидайте публично. (Shawn Wang, @swyx), вероятно, лучший способ оста­ это творить публично. Это означает делиться своей событий - По мнению Шона Вана ваться в курсе работой, мыслями и идеями с сообществом. Это может быть так же просто, как опубликовать информацию о своей работе в социальных сетях, или так же сложно, как написать пост в блоге или создать видео на YouTube. Делясь своими работами, вы можете получить обратную связь от сообщества, которая поможет вам улучшить свои навыки и получить более глубокое представление об исполь­ зуемых вами инструментах. Написание книги стало для меня отличным способом изучить React. Я многому научился от сообщества, и я смог поделиться своими знаниями с другими. Я на­ стоятельно рекомендую это!
360 Глава 11 В заключение: помните, что изучение React - это не просто освоение библиотеки, это постижение мировоззрения. Это мышление, основанное на разработке с помо­ щью компонентов, оптимизации производительности и постоянной адаптации к быстроменяющимся требованиям Интернета. Веб-разработка имеет светлое будущее, и React вместе со своим сообществом, не­ сомненно, сыграет значительную роль в его формировании. Независимо от того, являетесь ли вы опытным разработчиком или только начинаете, навыки и знания, которые вы почерпнете из этой книги, сослужат вам хорошую службу, когда вы продолжите свой путь в обширной и захватывающей области веб-разработки. Хочется, чтобы стало еще больше интуитивно понятных, производительных и ори­ ентированных на пользователя приложений React. Желаю вам успехов в будущем и благодарю вас за то, что вы стали частью этого приключения!
Предметный указатель Symbols F $$typeof, свойство Rеасt-элемента 95 $RC, функция 211 _ owner, свойство Rеасt-элемента 96 _store, свойство Rеасt-элемента 97 <, JSX-пparмa 74 FiberRootNode 121 Fiber-дepeвo 112, 113 FiЬеr-согласование 116 ◊ рендеринг 116 ◊ фиксация 119 - фаза компоновки 120 - фаза мутации 119 - эффекты 121 FiЬеr-согласователь 112, 225 Fiber-yзeл 114 Flux 58 Forget 146, 349, 350 А Abstract syпtax tree (AST) 71 AJAX 63 AngularJS 45 в Backbone 34 begin W ork, функция 117 G Gatsby 217 с н commitDeletion, функция 120 commitLayoutEffects, функция 120 commitMutationEffects, функция 120 commitRoot, функция 122 commitUnmount, функция 120 complete Work, функция 118 Control Props 172 Cross-site request forgery (CSRF) 189 handleBotRequest, функция Remix 276 handleBrowserRequest, функция Remix 277 handleRequest, функция Remix 276 Higher-order component (НОС) 162 D lmmer 157 lncremental static regeneration (ISR) 293 Dependency injection (DI) 47 Document object model (DOM) 29, 77 ◊ производительность 83 ◊ совместимость кроссбраузерная Don't Repeat Yourself56 Е ensureRootlsScheduled, Express.js 263 функция 232 J 89 jQuery 31 JSX (JavaScript Syntax eXtension) 63 ◊ выражения 75 ◊ недостатки 67 ◊ основы кода 68 ◊ преимущества 66
Предметный указатель 362 s к Keying 56 КnockoutJS 40 L Last in, first out (LIFO) 109 м Model-View-Controller (MVC) 34, 35 Model-View-ViewModel (MVVM) 41 N Next.js 216,282,291 Node.js 203 ◊ т Transpilation 73 type, свойство Rеасt-элемента 95 поток214 - двусторонний 204 203 для чтения 203 преобразующий 204 для записи u р Pragma 74 props, свойство Rеасt-элемента Search engine optimization (SEO) 185 Server-side rendering (SSR) 185 Solid 337 Source-to-source compilation 73 startTransition, функция 238 State Reducer 180 Suspense 150-152, 202, 205, 207 ◊ граница 152 Suspense boundary 152 Svelte 332 SwiftUI 67, 356 96 Q Qwik 338 R React.createElement, функция 98 React.forwardRef 170 React.memo 124 137 Rеасt-элемент 94 Reconciliation 53, 55, 78, 99, 105 ref, свойство Rеасt-элемента 96 Remix 216, 272, 292 renderToPipeaЬ\eStream 202 renderToReadaЬ\eStream 214 renderToString 199 useDeferredValue, хук 241 useFonnStatus, хук 289 useMemo, хук 130, 137, 139 useReducer, хук 153, 181 useRef, хук 318 useState, хук 53, 153 useSyncExterna\Store, хук 250 useTransition, хук 227, 237 V Virtual document object model (vDOM) 52, 77,93 Vue.js 327 w Work-in-progress tree 116 z Zone.js 330
Предметный указатель Компилятор А Анализатор лексический Антидребезг о 69 27 JIТ 68 71 ◊ кросс-компилятор ◊ нативный код 71 71 73 Батчинг. Сн. Обработка пакетная Композиция Безопасность данных Компонент: Буферизация 217 двойная 115 Внедрение зависимостей Водопад сетевой 47 186, 262 г Генерация кода Геттер 69, 71 175 193 232 Suspense 152 Гидратация клиентский ◊ серверный аккордеон ◊ высшего порядка о клиентский Лексер Граф: ◊ о 176 л Голодание Граница 56 162 288, 298 о контейнера 160 ◊ презентации 160 о серверный 282, 297, 298 о составной 178 ◊ управляемый 172 Кросс-компилятор 71 в 69 м 313 313 Макрозадача 231 Манипулирование ключами д 56 264 Маршрутизатор изоморфный Маршрутизация Данные, мутация: ◊ в о в о Next.js 285 Remix 279 Действие серверное Делегирование 285, 320 событий 308 абстрактное синтаксическое синтаксическое 69 в 264 Next.js 283 Remix 277 ◊ в ◊ на основе файловой системы Микрозадача 71 о компонентная ◊ ментальная о объектная Модель з Загрузка ленивая 14 7, 148, 149 Замедление 27 Запоминание 123 - 56 15 78 Представление - Контроллер 34 (DOM) 29, 77 виртуальная (vDOM) 52, 77, 93 производительность 83 совместимость кроссбраузерная 89 Модель документа объектная ◊ ◊ о Мутация данных: и Интерпретатор 72 к о в о в Next.js 285 Remix 279 о Каскад сетевой Обнаружение изменений Код, генерация Обработка пакетная Коллекция Оптимизация поисковая 186 69, 71 пропсов 174 264 231 Модель: Дерево: ◊ 363 Компиляция исходного кода в исходный Б ◊ 1 330 107 185
Предметный указатель 364 п с Парсинг69 Санация Патгерн: Сахар синтаксический ◊ Сериализация ◊ Control Props 172 State Reducer 180 ◊ проектирования программного обеспечения Планировщик 159 118, 233 Поток: ◊ Node.js214 браузера 214 Правило лексическое ◊ в ◊ в Angular 331 Vue 329 Слушатель события Подделка межсайтовых запросов ◊ 189 Согласование 23 53, 55, 78, 99, 105, 116 Сравнение: ◊ по значению ◊ по ссылке 128 129 Среда выполнения 72 Стек 109 69 т Прагма 74 Принцип DRY 56 Пропс 96 ◊ коллекция 174 ◊ рендеринга 170 Тип: ◊ нескалярный (ссылочный) ◊ скалярный (примитивный) Токен 69 Токенизация р 69 73 Транспиляция Разрыв 247 ф Реактивность: ◊ во время компиляции ◊ крупнозернистая ◊ мелкозернистая 335 343 337 Фиксация статическая Рендеринг Фреймворк Функция чистая Хук на стороне сервера ◊ полоса ◊ серверный 333 92 259 123 х 293 115 ◊ 116 Фрагмент документа Регенерация инкрементная Руны 74 306 Сигнал: 229 Полоса рендеринга 66 53, 130, 137, 153, 162, 168 185 э 233 263, 282, 299 Элемент React 94 Этап согласования 116 128 128
06 авторе Теджас Кумар (Tejas Kumar) пишет код для React с 2014 года, и неоднократно вы­ ступал на конференциях, семинарах и читал лекции по этой теме. Обладая богатым техническим опытом работы в нескольких стартапах, Теджас глубоко разобрался в основных концепциях React и с удовольствием использует свои знания для поощ­ рения, обучения и расширения возможностей других специалистов в области соз­ дания приложений React.

06 изображении Животное, enicura), изображенное на обложке, - это на обложке стройный искрохвост (Doricha пчелиная колибри, обитающая в высокогорных лесах Сальвадора, Гвате­ малы, Гондураса и Мексики. Эта колибри принадлежит к роду из трех родов семейства Trochilidae, Mellisugini, одному в которое входят все колибри. Стройный искрохвост имеет длину от (от менее одной десятой унции (менее 3 до 5 дюймов 2,83 г), что делает его одним из самых малень­ 7,62 до 12,7 см) и весит ких видов колибри. У них зеленая верхняя часть тела и белое брюшко. Самцы име­ ют переливающееся фиолетовое пятно на горле и длинный раздвоенный хвост. Эти колибри питаются в основном богатым сахаром нектаром и мелкими членистоно­ гими. Самцы очень привязаны к своей территории и будут прогонять незваных гос­ тей с мест кормежки. Самки строят гнезда из растительных волокон и паутины. Самка откладывает ца, которые она высиживает одна в течение мерно в возрасте 25 15-19 2 яй­ дней. Птенцы оперяются при­ дней. Стройный искрохвост имеет обширный ареал и многочисленную взрослую популя­ цию и, согласно Международному союзу охраны природы и природных ресурсов (Intemational Union for Conservation of Nature and Natural Resources, IUCN), является видом, вызывающим наименьшие опасения в плане истребления. Многие животные на обложках издательства O'Reilly находятся под угрозой исчезновения; все они важны для планеты. Иллюстрация на обложке выполнена Карен Монтгомери (Karen Montgomery) по (George Shaw мотивам старинной гравюры из книги Дж. Шоу "Общая зоология" "General zoology").
Теджас Кумар React. К вершинам мастерства Перевод с английского ТОО"АЛИСТ" Республика Казахстан, г. Астана, пр. Сарыарка, д. 17, ВП 010000, 30 Подписано в печать 06.02.25. Формат 70х100 1 / 16 . Печать офсетная. Усл. печ. л. 29.67. Тираж 1000 экз. Заказ № 12090. Отпечатано с готового оригинал-макета ООО "Принт-М", 142300, РФ, М.О., г. Чехов, ул. Полиграфистов, д.