Как незначительные решения влияют на структуру долговечных кодбаз на React
Пятнадцать привычек технического обслуживания для React-приложений, которые работают годами: читаемый код, сфокусированные компоненты, ограниченное состояние, минимум зависимостей, тесты и мониторинг.
Начало работы над проектом на React — это простая часть. Настоящее испытание наступает спустя месяцы или годы, когда у приложения появляется больше пользователей, функций, участников разработки и зависимостей, а несколько «временных» решений тихо превращаются в основу работы системы. В такой момент сам React редко бывает причиной проблемы; проблема заключается в том, чтобы сохранять понятность кодовой базы на фоне постоянных изменений вокруг неё. В этом руководстве рассматриваются пятнадцать привычек, сгруппированных по тем областям, где фактически накапливаются затраты на техническое обслуживание, чтобы вы могли распознать решения, которые со временем усугубляют ситуацию, превращая кодовую базу в то, чего никто не хочет трогать.
Код, написанный для следующего читателя
Предпочитайте понятный код умному
Компактные однострочные фрагменты кода, глубокие абстракции и вспомогательные функции, созданные для десятков гипотетических сценариев, кажутся полезными в момент их написания. Через шесть месяцев они превращаются в загадку, часто для самого автора, который уже не помнит причину их создания.
Альтернативой является намеренно простой код. Рассмотрим возможность отображения экрана, который может находиться в состоянии загрузки, иметь ошибку или быть готовым. Ранние возвраты делают каждое состояние явным, по одному условию за раз:
if (isLoading) {
return <LoadingState />;
}
Ветка обработки ошибок имеет такую же структуру, а успешный путь находится в конце:
if (error) {
return <ErrorState />;
}
return <Dashboard />;
Здесь нет ничего впечатляющего, и именно в этом суть: любой может за несколько секунд понять, какой компонент отображается и когда. В крупном приложении скорость, с которой следующий разработчик поймет ваш код, имеет гораздо большее значение, чем желание произвести впечатление на текущего.
Имена — это документация, которая никогда не устаревает
При написании кода присвоение имён кажется незначительной деталью, но через полгода при отладке оно становится чрезвычайно важным. Сравните поиск этого в кодовой базе:
handleData()
с поиском этого:
calculateMonthlyRevenue()
Второе имя позволяет сразу понять, что вычисляет функция, ещё до того, как открыть файл. Большие кодовые базы во многом являются каналами общения между разработчиками; точное название отражает цель функции, а неопределённое превращает каждого читателя в детектива.
Остерегайтесь генерических названий, которые появляются под давлением сроков:
data
item
temp
helper
value
thing
Да, thing действительно встречается в реальных кодовых базах. Полезное правило: если название подходит одинаково хорошо для любого файла проекта, скорее всего, оно недостаточно информативно для конкретного файла.
Границы компонентов и состояний
Чрезмерно большие компоненты становятся ресурсоёмкими
Почти в каждом долговечном проекте на React со временем появляется компонент, содержащий четыре знака в количестве строк. Обычно всё не начинается так: сначала их около 150, затем появляются модальные окна, фильтры, механизмы загрузки данных, проверки прав доступа, ещё одно модальное окно. В итоге, открыв файл, вы можете найти что-то вроде этого:
Dashboard.tsx
1,247 lines
Компоненты такого размера трудно понимать, тестировать, переиспользовать и отлаживать, к тому же изменять их рискованно, поскольку любые правки могут повлиять на состояние, от которого зависят другие части файла. Разделяйте код раньше, чем кажется необходимым. Вместо одного файла:
Dashboard.tsx
разделите экран на части, каждая из которых будет выполнять свою функцию:
DashboardHeader.tsx
DashboardStats.tsx
DashboardFilters.tsx
RecentActivity.tsx
DashboardTable.tsx
Теперь каждый файл имеет одну задачу и гораздо меньший радиус воздействия. Практический критерий: если вы не можете описать компонент одним предложением без использования нескольких слов «и», значит, его нужно разделить.
Используйте те шаблоны, которые вы уже видели, а не те, которые вы себе представляете
Использование повторно применимых компонентов — это хороший подход, но его легко забыть в меру. Распространенная ошибка заключается в том, что одна кнопка пытается выполнять функции всех кнопок в продукте:
<UniversalButton
type="primary"
variant="rounded"
size="medium"
iconPosition="left"
loadingStyle="spinner"
/>
Каждый новый параметр кажется безвредным, но в совокупности они создают огромное количество вариантов, которые никто полностью не тестирует, и в результате «повторно применимый» компонент оказывается более сложным в использовании, чем три небольшие, специализированные кнопки. Повторное использование ценно; преждевременная универсализация — нет.
Извлекайте общую абстракцию только тогда, когда видите реальное повторение шаблона, а не потому, что будущая страница может в ней нуждаться. Если такая потребность возникнет, вы сможете разработать абстракцию на основе реальных примеров, а не догадок.
Расставляйте состояние по его категориям
Управление состоянием часто ухудшается незаметно. Небольшое приложение начинается с состояния компонентов:
useState()
Затем добавляется общее состояние через контекст:
useContext()
В итоге проект содержит одновременно Redux, несколько контекстов, локальное состояние, состояние URL и серверное состояние, при этом не ясно, какой из них управляет боковой панелью. Каждый инструмент казался разумным при добавлении; отсутствует правило, определяющее, что должно находиться где. Простой способ восстановить порядок — классифицировать состояние по его природе. Локальное UI-состояние включает такие элементы:
modal open
dropdown selected
input value
Оно должно оставаться внутри компонента, который им пользуется. Серверное состояние — это данные, находящиеся на серверной части и кэшируемые лишь в браузере:
users
products
analytics
Для такой категории следует использовать библиотеку, предназначенную для получения, кэширования и повторной верификации удалённых данных, вместо ручного копирования ответов в глобальное хранилище. Если ваша команда рассматривает такой переход, варианты выбора описаны в нашем сравнении React Query и Redux для управления состоянием сервера. Наконец, истинно глобальное состояние включает небольшой список элементов:
theme
authenticated user
app-wide preferences
Сократите объём этой последней группы до минимума: любой компонент может читать или изменять глобальное состояние, поэтому чем меньше его, тем меньше ошибок, которые кажутся возникающими из ниоткуда. Фильтры, вкладки и пагинация часто должны находиться в URL, где они сохраняются при перезагрузке и могут быть общедоступными.
Структура и зависимости
Сделайте структуру папок предсказуемой
Представьте, что вы присоединяетесь к проекту и находите здесь, в корне, следующее:
src/
components/
shared/
common/
helpers/
utils/
services/
core/
misc/
new/
new2/
Куда следует помещать новый компонент профиля пользователя? Никто не может сказать, поэтому каждый разработчик выбирает по-своему, и структура продолжает меняться. Перекрывающиеся категории вроде shared, common, helpers и utils показывают, что никто не определил, что означает каждая из них.
Структура, основанная на функциях, устраняет большую часть этой неоднозначности, группируя код в соответствии с той частью продукта, которую он обслуживает:
features/
auth/
dashboard/
users/
billing/
Внутри каждой функции небольшой набор повторяющихся подпапок помогает сохранять порядок:
components/
hooks/
services/
types/
Теперь все, кто работает с финансовым учетом, знают, где находится соответствующий код, и при переписывании функционала изменяется всего один каталог вместо десяти. Могут использоваться и другие структуры (см. наше сравнение структур папок в React); главное — чтобы правила были предсказуемыми и зафиксированными.
Рассматривайте каждую зависимость как потенциальную проблему при обслуживании
Для добавления пакета требуется всего одна команда:
npm install something-cool
Проблемы проявляются позже: пакеты могут быть заброшены, вноситься критичные изменения, возникать проблемы с безопасностью и увеличиваться размер бандла, а ваша команда вынуждена постоянно обновлять код, который никто из её членов не читал.
Поэтому перед установкой спросите себя, действительно ли она вам нужна. Пакет, который экономит значительное количество усилий или решает по-настоящему сложную проблему, такую как обработка дат с учётом часовых поясов, заслуживает своего места. Тот же пакет, который просто форматирует строку, — нет.
Механизмы безопасности, развивающиеся вместе с приложением
Проверяйте те процессы, которые принесут наибольший ущерб в случае сбоя
В небольшом приложении достаточно просто пройтись по экранам перед выпуском. В крупном приложении изменение одной функции может нарушить систему оплаты по причинам, которые никто не может объяснить, и именно тогда оправдывают себя автоматизированные тесты: они позволяют уверенно изменять код, который вы не писали.
Вам не обязательно охватывать каждую деталь реализации. Сосредоточьтесь на тех пользователях, для которых сбой будет иметь серьёзные последствия:
- вход в систему
- процесс оформления заказа
- отправка важных форм
- проверки прав доступа
- расчёты, от которых зависит бизнес
Тесты не смогут доказать, что приложение безупречно; они выявляют очевидные регрессии раньше, чем это сделают пользователи. Тесты поведения, а не внутренних механизмов, также гораздо лучше переносят процесс рефакторинга.
Следите за медленным, постепенным снижением производительности
Большие приложения на React редко становятся медленными сразу после одного выпуска. Производительность ухудшается постепенно из-за каждого разумного решения: тяжелой зависимости, огромного изображения, ненужных перерисовок, десяти запросов на одном экране. Все это вместе может привести к тому, что загрузка панели управления займет пять секунд, поэтому постоянно следите за:
- перерисовкой компонентов, когда ничего из отображаемого не меняется
- постепенным увеличением размера пакета с каждым выпуском
- отправкой одного и того же запроса более одного раза на один экран
- компонентами, которые медленно рендерятся
- длинными списками, отображаемыми без виртуализации
- чрезмерно большими изображениями
Маленькие успехи накапливаются, и обнаружение регрессии сразу после её возникновения стоит гораздо дешевле, чем поиски её позже.
Целенаправленно создавайте сценарии неудач
Большинство усилий направляется на сценарии успешной работы, когда каждый запрос выполняется без проблем. Однако в реальных условиях происходят сбои соединений, отказы API, изменения прав доступа, и бэкенд возвращает данные в неожиданном формате. Пользователь должен видеть понятное сообщение с возможностью восстановления, например: «Мы не смогли загрузить ваши данные. Попробуйте ещё раз», а не грубую ошибку выполнения, такую как:
TypeError: Cannot read properties of undefined
В терминах React это означает явное указание состояний ошибок при загрузке данных, использование границ ошибок, чтобы один неисправный компонент не делал страницу пустой, а также механизмы повторной попытки там, где это целесообразно.
Рассматривайте мониторинг в производственных условиях как часть разработки
Запуск продукта — это не конец работы. В реальных условиях возникают проблемы, которые никогда не проявятся во время локальной разработки, и вам необходим контроль над:
- ошибки во время выполнения и места их возникновения
- запросы, выполняющиеся медленнее, чем ожидается
- неудачные вызовы API
- общая производительность страницы и взаимодействия
- то, как люди на самом деле используют продукт
Без этого отчет о баге — это просто «пользователь сказал, что вчера что-то сломалось». Трекинг ошибок и контекстуальные логи превращают его в стек-трейс и временную метку, с которыми можно работать.
Привычки, помогающие команде держаться вместе
Пишите документацию для уже работающих в команде людей
Документацию часто воспринимают как рутинную задачу по передаче информации, однако она помогает всем в команде уже сегодня, особенно при работе с сложными правами доступа, необычной архитектурой, интеграциями с сторонними сервисами и важными бизнес-правилами.
Не требуется длинное руководство. Краткие примечания о том, как работает аутентификация, как происходит передача данных, где находятся основные функции и почему были приняты определенные решения, экономят часы времени. Обращение к первоначальному разработчику становится бесполезным, как только он уходит, а в долгосрочных проектах это всегда происходит.
Рефакторинг — это рутинное обслуживание, а не признание неудачи
Рефакторинг не доказывает, что первоначальный код был плохим. Требования, команды и продукты меняются, и код, который подходил год назад, может больше не соответствовать поставленной задаче.
Опасность заключается в утверждении, что что-то «уже работает». Так же обстоит дело с трехногим стулом, пока кто-то на него не сядет. Небольшие, регулярные рефакторинги с использованием тестов позволяют контролировать технический долг.
Согласованность важнее личных предпочтений
Один разработчик пишет идентификаторы вот так:
camelCase
другой предпочитает вот это:
snake_case
А третьи каждые несколько недель придумывают новые шаблоны компонентов. Крупная команда не может эффективно работать, когда каждый файл соответствует вкусам его автора; поэтому необходимо сделать соблюдение единых стандартов автоматическим с помощью:
- инструмента проверки кода с установленными правилами
- автоматического форматтера
- документированных правил именования
- общедоступных, проверенных шаблонов для типовых задач
Кодовая база должна выглядеть как единый проект, а не результат споров десятка разработчиков по поводу структуры файлов.
Выбирайте архитектуру, которую ваша команда действительно может реализовать
Дискуссии об архитектуре — это любимое занятие: микросервисы, монорепозитории, Clean Architecture, доменно-ориентированное проектирование — у кого-то всегда есть готовая диаграмма. Но самый сложный вариант не всегда является лучшим. Хороший выбор должен соответствовать трем критериям:
- он решает реальные проблемы, с которыми вы сталкиваетесь
- вся команда может его объяснить
Простой дизайн, применяемый последовательно, всегда лучше блестящего дизайна, понятного только его создателю.
Почему всё это не вызывает восторга и почему это работает
Долгосрочное обслуживание меняет понимание того, что такое «хорошая разработка»: скорость написания кода становится менее важной по сравнению с читаемостью, легкостью будущих изменений, потребностями других разработчиков и поведением в производственных условиях. Вот чек-лист для работы:
- Компоненты остаются небольшими и однозадачными
- Глобальное состояние представляет собой короткий, продуманный список
- Названия отражают назначение
- У каждого нового пакета есть обоснование
- Структура папок соответствует одному задокументированному правилу
- Рефакторинг происходит постоянно
- У критически важных пользовательских процессов есть тесты
- Ошибки в производственной среде видны
- В случае равенства преимуществ побеждает более простой вариант
Эти правила не выглядят привлекательно, и, вероятно, именно поэтому они действуют столь долго. Чтобы узнать больше о практиках проектирования на уровне архитектуры, ознакомьтесь с десятью архитектурными привычками, которые обеспечивают поддерживаемость кодовых баз фронтенда в течение многих лет.
Основные выводы
- Большие приложения на React редко сталкиваются с проблемами из-за самого React; проблемы возникают из-за накопления мелких упрощений: один огромный компонент, одна загадочная утилита, один ненужный пакет, один временный хак.
- Поддерживаемость нельзя добавить в последний момент. Это сумма множества небольших решений, принимаемых постепенно.
- Самый подходящий момент для внедрения этих привычек — до появления проблем, когда разделение компонента или отказ от зависимости занимают всего несколько минут, а не недели.
Связанные статьи
- Десять архитектурных привычек, которые позволяют сохранять кодовые базы фронтенда пригодными к обслуживанию в течение многих лет — Рассматриваются структурные привычки, такие как оптимизация для удаления элементов, четкое моделирование потока данных и изоляция бизнес-логики, которые помогают кодовым базам оставаться пригодными к обслуживанию на протяжении многих лет изменений.
- Причины хранения данных, вывод последствий: проектирование минимального состояния в React — Научитесь выявлять избыточное состояние в React, заменять цепочки синхронизации, основанные на Effect, и булевы флаги на вычисляемые значения и объединения статусов, а также определять, где должно храниться состояние.