Когда речь идёт о разработке React-приложений, то, в плане архитектуры кода, маленькие проекты часто бывают более гибкими, чем большие. Нет ничего плохого в том, чтобы создавать такие проекты с использованием практических рекомендаций, нацеленных на более крупные приложения. Но всё это, в случае с маленькими проектами, может оказаться попросту ненужным. Чем меньше приложение — тем «снисходительнее» оно относится к использованию в нём простых решений, возможно — неоптимальных, но не требующих больших затрат времени на их реализацию.
Несмотря на это хотелось бы отметить, что некоторые рекомендации, которые будут даны в этом материале, нацелены на React-приложения любого масштаба.
Если вы никогда не создавали продакшн-приложения, то эта статья может помочь вам подготовиться к разработке крупномасштабных решений. Нечто подобное вполне может стать одним из ваших следующих проектов. Худшее, что может случиться с программистом, это когда он работает над проектом и понимает, что ему необходимо выполнить рефакторинг больших объёмов кода для улучшения масштабируемости и поддерживаемость приложения. Ещё хуже всё выглядит в том случае, если в проекте до рефакторинга не было модульных тестов.
Автор этого материала просит читателя поверить ему на слово. Он бывал в подобных ситуациях. Так, ему досталось несколько задач, которые нужно было решить за определённое время. Поначалу он думал, что всё у него получается превосходно. Источником подобных мыслей стало то, что его веб-приложение, после внесения изменений, продолжало работать, и при этом продолжало работать быстро. Он знал о том, как использовать Redux, о том, как наладить нормальное взаимодействие компонентов пользовательского интерфейса. Ему казалось, что он глубоко понимает концепции редьюсеров и действий. Он чувствовал себя неуязвимым.
Но тут подкралось будущее.
Через пару месяцев работы над приложением в него было добавлено более 15 новых возможностей. После этого проект вышел из-под контроля. Код, в котором использовалась библиотека Redux, стало очень тяжело поддерживать. Почему так случилось? Разве поначалу не казалось, что проект ожидает долгая и безоблачная жизнь?
Автор статьи говорит, что, задаваясь подобными вопросами, понял, что своими руками заложил в проект бомбу замедленного действия.
Библиотека Redux, если правильно использовать её в больших проектах, помогает, по мере роста таких проектов, сохранять их код в поддерживаемом состоянии.
Здесь будут даны 12 советов для тех, кто хочет разрабатывать масштабируемые React-приложения с использованием Redux.
1. Не размещайте код действий и констант в одном месте
Вы могли столкнуться с некоторыми руководствами по Redux, в которых константы и все действия размещают в одном и том же месте. Однако подобный подход, по мере роста приложения, быстро может привести к появлению проблем. Константы нужно хранить отдельно, например, в ./src/constants
. В результате для поиска констант придётся заглядывать лишь в одну папку, а не в несколько.
Кроме того, совершенно нормальным выглядит создание отдельных файлов, хранящих действия. Такие файлы инкапсулируют напрямую связанные друг с другом действия. Действия в одном файле, например, могут иметь сходства в плане того, с чем и как они используются.
Предположим, вы разрабатываете аркадную или ролевую игру и создаёте классы warrior
(воин), sorceress
(волшебница) и archer
(лучник). В такой ситуации добиться высокого уровня поддерживаемости кода можно, организовав действия следующим образом:
src/actions/warrior.js
src/actions/sorceress.js
src/actions/archer.js
Гораздо хуже будет, если всё попадёт в один файл:
src/actions/classes.js
Если приложение становится очень большим, то, возможно, ещё лучше будет воспользоваться примерно такой структурой разбиения кода по файлам:
src/actions/warrior/skills.js
src/actions/sorceress/skills.js
src/actions/archer/skills.js
Тут показан лишь небольшой фрагмент подобной структуры. Если мыслить шире и последовательно использовать этот подход, то в итоге получится примерно такой набор файлов:
src/actions/warrior/skills.js
src/actions/warrior/quests.js
src/actions/warrior/equipping.js
src/actions/sorceress/skills.js
src/actions/sorceress/quests.js
src/actions/sorceress/equipping.js
src/actions/archer/skills.js
src/actions/archer/quests.js
src/actions/archer/equipping.js
Вот как может выглядеть действие из файла src/actions/sorceress/skills
для объекта sorceress
:
import { CAST_FIRE_TORNADO, CAST_LIGHTNING_BOLT } from '../constants/sorceress'
export const castFireTornado = (target) => ({
type: CAST_FIRE_TORNADO,
target,
})
export const castLightningBolt = (target) => ({
type: CAST_LIGHTNING_BOLT,
target,
})
Вот содержимое файла src/actions/sorceress/equipping
:
import * as consts from '../constants/sorceress'
export const equipStaff = (staff, enhancements) => {...}
export const removeStaff = (staff) => {...}
export const upgradeStaff = (slot, enhancements) => {
return (dispatch, getState, { api }) => {
// Обратиться к слоту на экране обмундирования для того чтобы получить ссылку на посох волшебницы
const state = getState()
const currentEquipment = state.classes.sorceress.equipment.current
const staff = currentEquipment[slot]
const isMax = staff.level >= 9
if (isMax) {
return
}
dispatch({ type: consts.UPGRADING_STAFF, slot })
api.upgradeEquipment({
type: 'staff',
id: currentEquipment.id,
enhancements,
})
.then((newStaff) => {
dispatch({ type: consts.UPGRADED_STAFF, slot, staff: newStaff })
})
.catch((error) => {
dispatch({ type: consts.UPGRADE_STAFF_FAILED, error })
})
}
}
Причина, по которой мы организуем код подобным образом, заключается в том, что в проекты постоянно добавляются новые возможности. Это означает, что нам нужно быть готовыми к их появлению и при этом стремиться к тому, чтобы файлы не были бы перегружены кодом.
В самом начале работы над проектом подобное может показаться излишним. Но чем больше будет становиться проект, тем сильнее будет ощущаться сила такого подхода.
2. Не размещайте код редьюсеров в одном месте
Когда я вижу, что код моих редьюсеров превращаются в нечто подобное тому, что показано ниже, я понимаю, что мне нужно что-то менять.
const equipmentReducers = (state, action) => {
switch (action.type) {
case consts.UPGRADING_STAFF:
return {
...state,
classes: {
...state.classes,
sorceress: {
...state.classes.sorceress,
equipment: {
...state.classes.sorceress.equipment,
isUpgrading: action.slot,
},
},
},
}
case consts.UPGRADED_STAFF:
return {
...state,
classes: {
...state.classes,
sorceress: {
...state.classes.sorceress,
equipment: {
...state.classes.sorceress.equipment,
isUpgrading: null,
current: {
...state.classes.sorceress.equipment.current,
[action.slot]: action.staff,
},
},
},
},
}
case consts.UPGRADE_STAFF_FAILED:
return {
...state,
classes: {
...state.classes,
sorceress: {
...state.classes.sorceress,
equipment: {
...state.classes.sorceress.equipment,
isUpgrading: null,
},
},
},
}
default:
return state
}
}
Такой код, без сомнения, может очень скоро привести к большому беспорядку. Поэтому лучше всего поддерживать структуры работы с состоянием в как можно более простом виде, стремясь к минимальному уровню их вложенности. Можно, вместо этого, попытаться прибегнуть к композиции редьюсеров.
Полезным приёмом в работе с редьюсерами может стать создание редьюсера высшего порядка, который генерирует другие редьюсеры. Здесь можно почитать об этом подробнее.
3. Используйте информативные имена переменных
Именование переменных, на первый взгляд, может показаться элементарной задачей. Но на самом деле эта задача может оказаться одной из самых сложных.
Подбор имён переменных, в целом, имеет отношение к практическим рекомендациям по написанию чистого кода. Причина, по которой вообще существует такое понятие, как «имя переменной», заключается в том, что этот аспект разработки кода играет на практике очень важную роль. Неудачный подбор имён переменных — это верный способ навредить себе и членам своей команды в будущем.
Пытались ли вы когда-нибудь редактировать чужой код и сталкивались ли при этом со сложностями в понимании того, что именно делает этот код? Случалось ли вам запускать чужую программу и обнаруживать, что работает она не так, как ожидается?
Я взялся бы на спор доказать то, что вы в таких случаях встречались с так называемым «грязным кодом».
Если с подобным кодом приходится сталкиваться в крупных приложениях — то это просто кошмар. Такое, к сожалению, происходит довольно часто.
Вот один случай из жизни. Я редактировал код хука React из одного приложения и в этот момент мне прислали задание. Оно заключалось в том, чтобы реализовать в приложении возможность показа дополнительной информации о врачах. Эта информация должна была быть показана пациенту, который щёлкает по аватарке врача. Брать её нужно было из таблицы, на клиент она должна была попадать после обработки очередного запроса к серверу.
Задача это была несложная, основная проблема, с которой я столкнулся, заключалась в том, что мне пришлось потратить слишком много времени на то, чтобы найти, где именно в коде проекта находится то, что мне нужно.
Я поискал в коде по словам info
, dataToSend
, dataObject
, и по другим, которые, в моём представлении, связаны с данными, получаемыми с сервера. Через 5-10 минут мне удалось найти код, ответственный за работу с нужными мне данными. Объект, в котором они оказывались, был назван paymentObject
. В моём представлении объект, имеющий отношение к платежам, может содержать нечто вроде CVV-кода, номера кредитной карты, почтового индекса плательщика, и другие подобные сведения. В обнаруженном мной объекте было 11 свойств. Лишь три из них имели отношение к платежам: метод оплаты, идентификатор платёжного профиля и список кодов купонов.
Не улучшило ситуацию и то, что мне пришлось вносить в этот объект изменения, которые требовались для решения стоящей передо мной задачи.
Короче говоря, рекомендуется воздерживаться от использования непонятных имён для функций и переменных. Вот пример кода, в котором имя функции notify
не раскрывает её смысла:
import React from 'react'
class App extends React.Component {
state = { data: null }
// Кого уведомляем-то?
notify = () => {
if (this.props.user.loaded) {
if (this.props.user.profileIsReady) {
toast.alert(
'You are not approved. Please come back in 15 minutes or you will be deleted.',
{
position: 'bottom-right',
timeout: 15000,
},
)
}
}
}
render() {
return this.props.render({
...this.state,
notify: this.notify,
})
}
}
export default App
4. Не изменяйте структуры данных или типы в уже настроенных потоках данных приложений
Одной из самых больших ошибок, которые я когда-либо совершал, было изменение структуры данных в уже настроенном потоке данных приложения. Новая структура данных принесла бы огромный прирост производительности, так как она использовала быстрые методики поиска данных в объектах, хранящихся в памяти, вместо перебора массивов. Но было слишком поздно.
Прошу вас этого не делать. Пожалуй, нечто подобное может позволить себе лишь тот, кто абсолютно точно знает о том, на какие части приложения это может повлиять.
Какие последствия могут быть у подобного шага? Например, если нечто сначала было массивом, а потом стало объектом, это способно нарушить работу многих частей приложения. Я сделал огромную ошибку, полагая, что способен помнить обо всех местах кода, на которые может повлиять изменение представления структурированных данных. Однако в подобных случаях всегда находится какой-нибудь фрагмент кода, на который влияет изменение, и о котором никто не помнит.
5. Используйте сниппеты
Раньше я был фанатом редактора Atom, но перешёл на VS Code из-за того, что этот редактор, в сравнении с Atom, оказался невероятно быстрым. И он, при его скорости, поддерживает огромное количество самых разных возможностей.
Если вы тоже пользуетесь VS Code — рекомендую установить расширение Project Snippets. Это расширение позволяет программисту создавать собственные сниппеты для каждого рабочего пространства, используемого в некоем проекте. Это расширение работает так же, как и встроенный в VS Code механизм Use Snippets. Разница заключается в том, что при работе с Project Snippets в проекте создают папку .vscode/snippets/
. Выглядит это так, как показано на следующем рисунке.
Содержимое папки .vscode/snippets/
6. Создавайте модульные, сквозные и интеграционные тесты
По мере роста размеров приложения программисту становится всё страшнее редактировать код, который не покрыт тестами. Например, может случиться так, что некто отредактировал код, хранящийся в src/x/y/z/
, и решил отправить его в продакшн. Если при этом внесённые изменения влияют на те части проекта, о которых программист не подумал, всё может закончиться ошибкой, с которой столкнётся реальный пользователь. Если в проекте имеются тесты, программист узнает об ошибке задолго до того, как код попадёт в продакшн.
7. Проводите мозговые штурмы
Программисты, в ходе внедрения в проекты новых возможностей, часто отказываются от мозговых штурмов. Происходит так из-за того, что подобная деятельность не связана с написанием кода. Особенно часто так случается тогда, когда на выполнение задания отводится совсем немного времени.
А зачем, кстати, вообще проводить мозговые штурмы в ходе разработки приложений?
Дело в том, что чем сложнее становится приложение, тем больше внимания программистам приходится уделять его отдельным частям. Мозговые штурмы помогают сократить время, необходимое на рефакторинг кода. После их проведения программист оказывается вооружённым знанием о том, что может пойти не так в ходе доработки проекта. Часто программисты, занимаясь развитием приложения, даже не утруждают себя тем, чтобы хоть немного подумать о том, как сделать всё наилучшим образом.
Именно поэтому мозговые штурмы — это очень важно. В ходе подобного мероприятия программист может обдумать архитектуру кода, поразмыслить о том, как внести в программу необходимые изменения, проследить жизненный цикл этих изменений, создать стратегию работы с ними. Не стоит заводить у себя привычку держать все планы исключительно в собственной голове. Так поступают программисты, которые чрезмерно уверены в себе. Но помнить абсолютно всё просто невозможно. И, как только что-нибудь будет сделано неправильно, проблемы будут появляться одна за другой. Это — принцип домино в действии.
Мозговые штурмы полезны и в командах. Например, если в ходе работы кто-то столкнётся с проблемой, он может обратиться к материалам мозгового штурма, так как возникшая у него проблема вполне могла быть уже обдумана. Заметки, которые делают в ходе мозгового штурма, вполне могут играть роль плана решения задачи. Этот план позволяет чётко оценивать объём выполненных работ.
8. Создавайте макеты приложений
Если вы собираетесь приступить к разработке приложения — вам нужно принять решение о том, как оно будет выглядеть, и о том, как с ним будут взаимодействовать пользователи. Это означает, что вам нужно будет создать макет приложения. Для этого можно воспользоваться различными инструментами.
Moqups — это одно из средств для создания макетов приложений, о котором мне часто приходится слышать. Это — быстрый инструмент, созданный средствами HTML5 и JavaScript и не предъявляющий особых требований к системе.
Создание макета приложения значительно облегчает и ускоряет процесс разработки. Макет даёт разработчику сведения о взаимосвязи отдельных частей приложения, и о том, какие именно данные будут выводиться на его страницах.
9. Планируйте поток данных в приложениях
Почти каждый компонент вашего приложения будет связан с некими данными. Некоторые компоненты будут использовать собственные источники данных, но большинство компонентов получает данные от сущностей, находящихся выше их в иерархии компонентов. Для тех частей приложения, в которых одни и те же данные совместно используются несколькими компонентами, полезно предусмотреть некое централизованное хранилище информации, расположенное на верхнем уровне иерархии. Именно в подобных ситуациях неоценимую помощь разработчику способна оказать библиотека Redux.
Рекомендую в ходе работы над приложением составлять схему, демонстрирующую пути, по которым в этом приложении перемещаются данные. Это поможет в создании чёткой модели приложения, причём, речь идёт и о коде, и о восприятии приложения программистом. Подобная модель поможет, кроме того, в создании редьюсеров.
10. Используйте функции доступа к данным
По мере роста размеров приложения растёт и количество его компонентов. А когда растёт количество компонентов, то же самое происходит и с частотой использования селекторов (react-redux ^v7.1) или mapStateToProps
. Предположим, вы обнаруживаете, что ваши компоненты или хуки часто обращаются к фрагментам состояния в различных частях приложения с использованием конструкции наподобие useSelector((state) => state.app.user.profile.demographics.languages.main)
. Если так — это значит, что вам нужно подумать о создании функций доступа к данным. Файлы с такими функциями стоит хранить в общедоступном месте из которого их могут импортировать компоненты и хуки. Подобные функции могут быть фильтрами, парсерами, или любыми другими функциями для трансформации данных
Вот несколько примеров.
Например, в src/accessors
может присутствовать такой код:
export const getMainLanguages = (state) =>
state.app.user.profile.demographics.languages.main
Вот версия с использованием connect
, которая может быть расположена по пути src/components/ViewUserLanguages
:
import React from 'react'
import { connect } from 'react-redux'
import { getMainLanguages } from '../accessors'
const ViewUserLanguages = ({ mainLanguages }) => (
<div>
<h1>Good Morning.</h1>
<small>Here are your main languages:</small>
<hr />
{mainLanguages.map((lang) => (
<div>{lang}</div>
))}
</div>
)
export default connect((state) => ({
mainLanguages: getMainLanguages(state),
}))(ViewUserLanguages)
Вот версия, в которой применяется useSelector
, находящаяся по адресу src/components/ViewUserLanguages
:
import React from 'react'
import { useSelector } from 'react-redux'
import { getMainLanguages } from '../accessors'
const ViewUserLanguages = ({ mainLanguages }) => {
const mainLanguages = useSelector(getMainLanguages)
return (
<div>
<h1>Good Morning.</h1>
<small>Here are your main languages:</small>
<hr />
{mainLanguages.map((lang) => (
<div>{lang}</div>
))}
</div>
)
}
export default ViewUserLanguages
Кроме того, стремитесь к тому, чтобы подобные функции были бы иммутабельными, лишёнными побочных эффектов. Узнать о том, почему я даю такую рекомендацию, можно здесь.
11. Управляйте потоком данных в свойствах с помощью деструктурирования и синтаксиса spread
Каковы преимущества использования конструкции props.something
перед конструкцией something
?
Вот как это выглядит без использования деструктурирования:
const Display = (props) => <div>{props.something}</div>
Вот — то же самое, но уже с использованием деструктурирования:
const Display = ({ something }) => <div>{something}</div>
Применение деструктурирования улучшает читабельность кода. Но этим его позитивное влияние на проект не ограничивается. Применяя деструктурирование, программист вынужден принимать решения о том, что именно получает компонент, и что именно он выводит. Это избавляет того, кому приходится редактировать чужой код, от необходимости просматривать каждую строчку метода render
в поиске всех свойств, которые использует компонент.
Кроме того, такой подход даёт полезную возможность задавать значения свойств по умолчанию. Делается это в самом начале кода компонента и избавляет от необходимости написания дополнительного кода в теле компонента:
const Display = ({ something = 'apple' }) => <div>{something}</div>
Возможно, раньше вы видели что-то подобное следующему примеру:
const Display = (props) => (
<Agenda {...props}>
{' '}
// перенаправление других свойств компоненту Agenda
<h2><font color="#000">Today is {props.date}</font></h2>
<hr />
<div>
<h3><font color="#000">▍Here your list of todos:</font></h3>
{props.children}
</div>
</Agenda>
)
Подобные конструкции непросто читать, но это — не единственная их проблема. Так, здесь имеется ошибка. Если приложение выводит и дочерние компоненты, то props.children
отображается на экране дважды. Если работа над проектом ведётся в команде и члены команды недостаточно внимательны, вероятность возникновения подобных ошибок довольно-таки высока.
Если вместо этого деструктурировать свойства, то код компонента окажется понятнее, а вероятность возникновения ошибок снизится:
const Display = ({ children, date, ...props }) => (
<Agenda {...props}>
{' '}
// перенаправление других свойств компоненту Agenda
<h2><font color="#000">Today is {date}</font></h2>
<hr />
<div>
<h3><font color="#000">▍Here your list of todos:</font></h3>
{children}
</div>
</Agenda>
)
Итоги
В этом материале мы рассмотрели 12 рекомендаций для тех, кто разрабатывает React-приложения с использованием Redux. Надеемся, вы нашли здесь что-то такое, что вам пригодится.
Уважаемые читатели! Какие советы вы добавили бы к тем, что приведены в этой статье?
Автор: ru_vds