Это статья о моих любимых способах создания SPA (одностраничных веб-приложений) с множеством технологий, которые мне нравятся, созданных очень умными людьми, и о том, почему я использую их немного по-другому.

Путь реакции

После многолетнего использования Angular для всех моих интерфейсных проектов я посмотрел Пит Хант: React: переосмысление лучших практик, в котором рассказывается о многих вещах, которые идут не так, как надо в типичном большом проекте Angular, и я был поражен тем, как Я был согласен практически со всем, что он сказал.

Шаблоны отделяют технологии и не касаются. И они делают это, будучи преднамеренно слабыми.

— Пит Хант

Итак, я проверил React и был очень впечатлен. Я обнаружил, что при написании нескольких компонентов React я бы написал одну монолитную директиву Angular только потому, что это было намного проще. Я также начал вставлять свой JSX и специфический для компонента CSS в те же файлы, что и остальная часть компонента, и обнаружил, что мне это нравится намного больше. В Angular я бы разделил их все, и каждый раз, когда мне нужно было изменить что-то видимое на экране, каждый раз менялось бы как минимум 3 файла (CSS, HTML, JS). Но теперь с этой новой философией все было в одном месте, и изменения проходили более плавно. Более того, я обнаружил, что обычно могу вытащить компонент React из одного проекта и поместить его в другой проект, и это сработает мгновенно. Сделать это с помощью директивы Angular, где части разбросаны по нескольким разным файлам, немного сложнее.

Еще один способ, с помощью которого React действительно хорошо справляется с истинным разделением задач, — это активное поощрение одностороннего потока данных. Родительские объекты передают свои данные дочерним объектам, используя «реквизиты», которые очень похожи на свойства HTML.

<StarRatings numStars=3/>

Так что, возможно, это что-то вроде компонента рейтинга фильмов Netflix, и родительский компонент сообщает ему, что большинство людей оценивают эту вещь на 3, поэтому пока покажите 3 пользователю 3 звезды. Но подождите, этот компонент StarRatings также должен заботиться о таких вещах, как то, что происходит, когда пользователь нажимает на новый звездный рейтинг. Родительскому компоненту, вероятно, нужно знать об этом по разным причинам, например, может быть, родительский компонент синхронизирует состояние с сервером (подробнее об этом позже) или другими дочерними компонентами. Так что же является хорошим способом убедиться, что родитель обновляется, не связывая чрезмерно родительский и дочерний компоненты? В конце концов, нам также нужно использовать этот же компонент StarRatings на странице обслуживания клиентов под заголовком «Как вы оценили свое обслуживание клиентов?», и исходные эффекты там будут совсем другими. Что ж, оказывается, реквизит — действительно хорошее место для этого. Родительский компонент может по-прежнему отвечать за поток данных, но просто сообщает дочернему компоненту, какую функцию вызывать, когда что-то меняется.

<StarRatings numStars=3 onChange={this.handleStarRatingsChange}/>

Таким образом, в одном месте родителем будет компонент Movie, а его функция handleChange сохранит новый рейтинг фильма пользователя и некоторые другие вещи. Но в другом месте родителем будет компонент Support, и его функция handleChange будет сообщать о производительности представителя customerService в центральную базу данных. Все красиво и развязано.

История данных

Таким образом, несмотря на то, что в React есть блестящая система разделения задач между компонентами, одной вещи, которой у него *нет*, является история данных. React сам по себе не имеет абсолютно никаких предпочтений в отношении того, как вы загружаете данные в свое приложение и как вы делитесь этими данными, загружаемыми между вашими представлениями, или даже в отношении того, какие модели вы используете для хранения всех этих данных. Таким образом, люди начали делать много разных вещей, например, использовать jQuery или Backbone.js, но они также начали просить у Facebook совета. В конце концов Facebook любезно поделился своей основной философией по этому вопросу в форме Архитектуры потока, где, к удивлению, удивлению, они распространили акцент на однонаправленный поток данных на уровень модели. Flux работал действительно хорошо, но это была скорее архитектура, чем что-либо еще, и, возможно, она была слишком сложной для одностраничного веб-приложения среднего размера. Наконец-то суть Flux уварилась в несколько более простой форме до библиотеки Redux.

В основе Redux лежат 3 простые идеи:

1. Состояние вашего приложения хранится в дереве объектов внутри одного хранилища (модели)

2. Единственный способ мутировать состояние — создать действие, объект, описывающий, что произошло. (паб)

3. Чтобы указать, как дерево состояний преобразуется действиями, вы пишете чистые редукторы (функции).

Оказывается, это отлично работает отчасти потому, что дает вам одно известное местоположение для всех ваших данных (модели), хорошо отделенное от всего вашего кода представления (компоненты React). Поскольку это широкое приложение, оно автоматически синхронизирует ваши данные между всеми вашими представлениями. И, поскольку это хорошо известный компонент с четко определенной структурой и API, он позволяет использовать несколько довольно плохих инструментов разработки.

Маршрутизация и мой левый поворот

Наконец, есть маршрутизация — маршрутизация — это то, что мы решили назвать компонентом JavaScript, который синхронизирует URL-адрес браузера с компонентом, который вы визуализируете. В наши дни маршрутизаторы довольно распространены среди фреймворков, а React-Router — это сторонняя реализация маршрутизатора для React с некоторым основательным мышлением и кодом.

И здесь, я думаю, у меня самое большое несогласие с общепринятым мнением. Если вы были очень внимательны, то, возможно, заметили, что я упомянул два фундаментальных источника истины. 1. Магазин Redux, который по философии должен поддерживать все состояние приложения в объекте, и 2. URL-адрес.

Есть несколько способов, которыми люди пытались синхронизировать эти вещи, включая относительно глубокие и относительно простые библиотеки, которые синхронизируют состояние маршрутизатора и Redux Store. Я хотел бы предложить альтернативный метод, который я считаю более простым и понятным.

Компонент контейнера

Так что мне очень нравится прохождение потока данных в React. На мой взгляд, идеальное взаимодействие с компонентами должно выглядеть примерно так:

var magicalApp = createMagicalApp(
    <Route path="/">
        <HomeComponent route={"/"} homeData={store.getHomeData()})/>
    </Route>

Теперь есть много причин, по которым это не сработает, но самое приятное в этом то, что вы можете посмотреть на JSX и *сразу* увидеть, какие реквизиты передаются в HomeComponent. За кулисами нет никакого волшебства, и если вы знаете React, вы сразу его понимаете. Черт возьми, если вы немного знаете HTML и JavaScript, вы все равно в основном это понимаете.

Итак, как близко мы можем подобраться? Что ж, в моих реальных приложениях я начал использовать компонент-контейнер. Компонент-контейнер собирает данные, необходимые для определенного представления, из немного запутанного реального мира, который содержит данные из разных мест, таких как URL, localStorage и хранилище Redux, и предоставляет их в качестве простых реквизитов для фактических компонентов представления внизу. Вот вымышленный пример страницы «Контейнер для учетной записи». На странице учетной записи пользователь просматривает информацию об учетных записях и обновляет учетные записи. В приложении также можно перейти на новую страницу учетной записи для другой учетной записи, что обновит реквизиты маршрутизации:

Итак, после этих 50 (хорошо разнесенных) строк кода мы переходим к самой интересной части. Сам компонент AccountPage — чистый, чистый, красивый односторонний поток данных React с реквизитами. И каждый компонент, содержащийся в AccountPage, будет таким же. AccountPage может быть настолько сложным или простым, насколько он хочет, как в представлении, так и в глубине своего стека компонентов. Все они будут обновляться в течение стандартного жизненного цикла React без необходимости заморачиваться синхронизацией состояний.

Плюсы и минусы стандартных библиотек

Плюсы:

  • Код очень читабелен. Здесь очень мало магии. Ничто не вводится и не вытягивается из контекста за кулисами.
  • Код ViewComponent также чрезвычайно удобочитаем и полностью соответствует идеальному потоку данных жизненного цикла React.
  • Меньше библиотек для отслеживания в вашей ментальной модели при работе над приложением
  • Меньше дублирования данных — данные отсутствуют как в URL, так и в Redux, что требует синхронизации. Данные URL остаются в месте URL. Данные LocalStorage остаются в LocalStorage (упражнение осталось для читателя). Данные Redux остаются в Redux

Минусы:

  • Не все данные обычно доступны через API-интерфейсы Redux, и некоторые действия, влияющие на приложение (например, маршрутизация или изменения localStorage), не выполняются с помощью действий. Я считаю, что это приемлемый компромисс. Я бы предпочел не повторять усилия через синхронизацию. По моему опыту, это добавляет точки отказа. Если предпочтительнее отлаживать состояние приложения путем наблюдения за отправками действий во всех случаях, вы можете просто отправить LOG_ROUTE_CHANGE_REQUEST или аналогичный запрос, и запрос на изменение маршрута будет отображаться в инструментах разработки, не полагаясь на синхронизацию.
  • В контейнере повторяется чуть больше стандартного кода, чем было бы необходимо при использовании react-redux. На мой взгляд, небольшое увеличение стандартного кода в этом случае допустимо, потому что это значительно сокращает «волшебство» подключения компонента к хранилищу Redux. Он также очень подробно показывает точки жизненного цикла React, где взаимодействует Redux, что я считаю полезным.

Итак, в заключение: React — это круто, Redux — это круто, React-Router — это круто, и я большой поклонник всех людей, которые сделали это возможным. Я использую их немного иначе, чем обычно, и я хотел бы услышать любые отзывы о компромиссах, которые возникают из-за этих дизайнерских решений!