- PVSM.RU - https://www.pvsm.ru -
Когда речь идёт о разработке React-приложений, то, в плане архитектуры кода, маленькие проекты часто бывают более гибкими, чем большие. Нет ничего плохого в том, чтобы создавать такие проекты с использованием практических рекомендаций, нацеленных на более крупные приложения. Но всё это, в случае с маленькими проектами, может оказаться попросту ненужным. Чем меньше приложение — тем «снисходительнее» оно относится к использованию в нём простых решений, возможно — неоптимальных, но не требующих больших затрат времени на их реализацию.
Несмотря на это хотелось бы отметить, что некоторые рекомендации, которые будут даны в этом материале, нацелены на React-приложения любого масштаба.
Если вы никогда не создавали продакшн-приложения, то эта статья может помочь вам подготовиться к разработке крупномасштабных решений. Нечто подобное вполне может стать одним из ваших следующих проектов. Худшее, что может случиться с программистом, это когда он работает над проектом и понимает, что ему необходимо выполнить рефакторинг больших объёмов кода для улучшения масштабируемости и поддерживаемость приложения. Ещё хуже всё выглядит в том случае, если в проекте до рефакторинга не было модульных тестов.
Автор этого материала просит читателя поверить ему на слово. Он бывал в подобных ситуациях. Так, ему досталось несколько задач, которые нужно было решить за определённое время. Поначалу он думал, что всё у него получается превосходно. Источником подобных мыслей стало то, что его веб-приложение, после внесения изменений, продолжало работать, и при этом продолжало работать быстро. Он знал о том, как использовать Redux, о том, как наладить нормальное взаимодействие компонентов пользовательского интерфейса. Ему казалось, что он глубоко понимает концепции редьюсеров и действий. Он чувствовал себя неуязвимым.
Но тут подкралось будущее.
Через пару месяцев работы над приложением в него было добавлено более 15 новых возможностей. После этого проект вышел из-под контроля. Код, в котором использовалась библиотека Redux, стало очень тяжело поддерживать. Почему так случилось? Разве поначалу не казалось, что проект ожидает долгая и безоблачная жизнь?
Автор статьи говорит, что, задаваясь подобными вопросами, понял, что своими руками заложил в проект бомбу замедленного действия.
Библиотека Redux, если правильно использовать её в больших проектах, помогает, по мере роста таких проектов, сохранять их код в поддерживаемом состоянии.
Здесь будут даны 12 советов для тех, кто хочет разрабатывать масштабируемые React-приложения с использованием Redux.
Вы могли столкнуться с некоторыми руководствами по 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 })
})
}
}
Причина, по которой мы организуем код подобным образом, заключается в том, что в проекты постоянно добавляются новые возможности. Это означает, что нам нужно быть готовыми к их появлению и при этом стремиться к тому, чтобы файлы не были бы перегружены кодом.
В самом начале работы над проектом подобное может показаться излишним. Но чем больше будет становиться проект, тем сильнее будет ощущаться сила такого подхода.
Когда я вижу, что код моих редьюсеров превращаются в нечто подобное тому, что показано ниже, я понимаю, что мне нужно что-то менять.
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
}
}
Такой код, без сомнения, может очень скоро привести к большому беспорядку. Поэтому лучше всего поддерживать структуры работы с состоянием в как можно более простом виде, стремясь к минимальному уровню их вложенности. Можно, вместо этого, попытаться прибегнуть к композиции редьюсеров.
Полезным приёмом в работе с редьюсерами может стать создание редьюсера высшего порядка, который генерирует другие редьюсеры. Здесь [2] можно почитать об этом подробнее.
Именование переменных, на первый взгляд, может показаться элементарной задачей. Но на самом деле эта задача может оказаться одной из самых сложных.
Подбор имён переменных, в целом, имеет отношение к практическим рекомендациям по написанию чистого кода. Причина, по которой вообще существует такое понятие, как «имя переменной», заключается в том, что этот аспект разработки кода играет на практике очень важную роль. Неудачный подбор имён переменных — это верный способ навредить себе и членам своей команды в будущем.
Пытались ли вы когда-нибудь редактировать чужой код и сталкивались ли при этом со сложностями в понимании того, что именно делает этот код? Случалось ли вам запускать чужую программу и обнаруживать, что работает она не так, как ожидается?
Я взялся бы на спор доказать то, что вы в таких случаях встречались с так называемым «грязным кодом».
Если с подобным кодом приходится сталкиваться в крупных приложениях — то это просто кошмар. Такое, к сожалению, происходит довольно часто.
Вот один случай из жизни. Я редактировал код хука 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
Одной из самых больших ошибок, которые я когда-либо совершал, было изменение структуры данных в уже настроенном потоке данных приложения. Новая структура данных принесла бы огромный прирост производительности, так как она использовала быстрые методики поиска данных в объектах, хранящихся в памяти, вместо перебора массивов. Но было слишком поздно.
Прошу вас этого не делать. Пожалуй, нечто подобное может позволить себе лишь тот, кто абсолютно точно знает о том, на какие части приложения это может повлиять.
Какие последствия могут быть у подобного шага? Например, если нечто сначала было массивом, а потом стало объектом, это способно нарушить работу многих частей приложения. Я сделал огромную ошибку, полагая, что способен помнить обо всех местах кода, на которые может повлиять изменение представления структурированных данных. Однако в подобных случаях всегда находится какой-нибудь фрагмент кода, на который влияет изменение, и о котором никто не помнит.
Раньше я был фанатом редактора Atom, но перешёл на VS Code из-за того, что этот редактор, в сравнении с Atom, оказался невероятно быстрым. И он, при его скорости, поддерживает огромное количество самых разных возможностей.
Если вы тоже пользуетесь VS Code — рекомендую установить расширение Project Snippets [3]. Это расширение позволяет программисту создавать собственные сниппеты для каждого рабочего пространства, используемого в некоем проекте. Это расширение работает так же, как и встроенный в VS Code механизм Use Snippets. Разница заключается в том, что при работе с Project Snippets в проекте создают папку .vscode/snippets/
. Выглядит это так, как показано на следующем рисунке.
Содержимое папки .vscode/snippets/
По мере роста размеров приложения программисту становится всё страшнее редактировать код, который не покрыт тестами. Например, может случиться так, что некто отредактировал код, хранящийся в src/x/y/z/
, и решил отправить его в продакшн. Если при этом внесённые изменения влияют на те части проекта, о которых программист не подумал, всё может закончиться ошибкой, с которой столкнётся реальный пользователь. Если в проекте имеются тесты, программист узнает об ошибке задолго до того, как код попадёт в продакшн.
Программисты, в ходе внедрения в проекты новых возможностей, часто отказываются от мозговых штурмов. Происходит так из-за того, что подобная деятельность не связана с написанием кода. Особенно часто так случается тогда, когда на выполнение задания отводится совсем немного времени.
А зачем, кстати, вообще проводить мозговые штурмы в ходе разработки приложений?
Дело в том, что чем сложнее становится приложение, тем больше внимания программистам приходится уделять его отдельным частям. Мозговые штурмы помогают сократить время, необходимое на рефакторинг кода. После их проведения программист оказывается вооружённым знанием о том, что может пойти не так в ходе доработки проекта. Часто программисты, занимаясь развитием приложения, даже не утруждают себя тем, чтобы хоть немного подумать о том, как сделать всё наилучшим образом.
Именно поэтому мозговые штурмы — это очень важно. В ходе подобного мероприятия программист может обдумать архитектуру кода, поразмыслить о том, как внести в программу необходимые изменения, проследить жизненный цикл этих изменений, создать стратегию работы с ними. Не стоит заводить у себя привычку держать все планы исключительно в собственной голове. Так поступают программисты, которые чрезмерно уверены в себе. Но помнить абсолютно всё просто невозможно. И, как только что-нибудь будет сделано неправильно, проблемы будут появляться одна за другой. Это — принцип домино в действии.
Мозговые штурмы полезны и в командах. Например, если в ходе работы кто-то столкнётся с проблемой, он может обратиться к материалам мозгового штурма, так как возникшая у него проблема вполне могла быть уже обдумана. Заметки, которые делают в ходе мозгового штурма, вполне могут играть роль плана решения задачи. Этот план позволяет чётко оценивать объём выполненных работ.
Если вы собираетесь приступить к разработке приложения — вам нужно принять решение о том, как оно будет выглядеть, и о том, как с ним будут взаимодействовать пользователи. Это означает, что вам нужно будет создать макет приложения. Для этого можно воспользоваться различными инструментами.
Moqups [4] — это одно из средств для создания макетов приложений, о котором мне часто приходится слышать. Это — быстрый инструмент, созданный средствами HTML5 и JavaScript и не предъявляющий особых требований к системе.
Создание макета приложения значительно облегчает и ускоряет процесс разработки. Макет даёт разработчику сведения о взаимосвязи отдельных частей приложения, и о том, какие именно данные будут выводиться на его страницах.
Почти каждый компонент вашего приложения будет связан с некими данными. Некоторые компоненты будут использовать собственные источники данных, но большинство компонентов получает данные от сущностей, находящихся выше их в иерархии компонентов. Для тех частей приложения, в которых одни и те же данные совместно используются несколькими компонентами, полезно предусмотреть некое централизованное хранилище информации, расположенное на верхнем уровне иерархии. Именно в подобных ситуациях неоценимую помощь разработчику способна оказать библиотека Redux [5].
Рекомендую в ходе работы над приложением составлять схему, демонстрирующую пути, по которым в этом приложении перемещаются данные. Это поможет в создании чёткой модели приложения, причём, речь идёт и о коде, и о восприятии приложения программистом. Подобная модель поможет, кроме того, в создании редьюсеров.
По мере роста размеров приложения растёт и количество его компонентов. А когда растёт количество компонентов, то же самое происходит и с частотой использования селекторов (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
Кроме того, стремитесь к тому, чтобы подобные функции были бы иммутабельными, лишёнными побочных эффектов. Узнать о том, почему я даю такую рекомендацию, можно здесь [6].
Каковы преимущества использования конструкции 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
Источник [9]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/javascript/321296
Ссылки в тексте:
[1] Image: https://habr.com/ru/company/ruvds/blog/456336/
[2] Здесь: https://redux.js.org/recipes/reducing-boilerplate#generating-reducers
[3] Project Snippets: https://marketplace.visualstudio.com/items?itemName=rebornix.project-snippets
[4] Moqups: https://moqups.com/
[5] Redux: https://redux.js.org/
[6] здесь: https://redux.js.org/faq/immutable-data#why-is-immutability-required-by-redux
[7] Image: https://ruvds.com/ru-rub/#order
[8] Image: https://ruvds.com/ru-rub/news/read/104
[9] Источник: https://habr.com/ru/post/456336/?utm_source=habrahabr&utm_medium=rss&utm_campaign=456336
Нажмите здесь для печати.