/
Автор: Кумар Т.
Теги: языки программирования компьютерные технологии программирование язык программирования javascript
ISBN: 978-601-08-4834-4
Год: 2025
Текст
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,
РФ, М.О., г. Чехов, ул. Полиграфистов, д.