SoundCloud клиент на React+Redux

В начале 2016 года я глубоко погрузился в мир ReactJS. Я читал тонны статей про React и его среду окружения, особенно Redux. Некоторые мои коллеги использовали его в сторонних проектах и на теоретическом уровне я мог бы поучаствовать с ними в дискуссиях.

На тот момент, в моей компании основной упор был на Angular. Так как у нас он используется в довольно большой кодовой базе, мы знаем о его недостатках. В 2015 году у нас уже была принята собственная flux-архитектура в Angular приложениях с использованием хранилищ (store) и однонаправленным потоком данных. Разумеется мы были хорошо осведомлены обо всех изменениях окружающей среды React в ближайшем времени.

Разработка FaveSound — простого клиента для SoundCloud, заняла у меня несколько недель. Являясь активным пользователем SoundCloud я был убеждён, что должен сделать собственный клиент для него на ReactJS + Redux.

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

В конце этого учебника вы можете рассчитывать на запуск React + Redux приложения, которое использует SoundCloud API. Через это приложение вы сможете заходить на SoundCloud под своим аккаунтом, смотреть список ваших последних треков и слушать их в браузере. Кроме того, вы узнаете много нового о таких инструментах как Webpack и Babel.

Содержание

  1. Проект с нуля
  2. Зависимости
  3. Давайте начнём
  4. Настройка Webpack
    1. Hot Reloading
  5. Babel
  6. Первый React Component
  7. Настройка тестов
  8. Redux
    1. Redux Cycle
    2. Отправка действий (Dispatching an Action)
    3. Константы типов действий (Constant Action Type)
    4. Создатели действий (Action Creators)
    5. Редьюсеры (Reducers)
    6. Store с глобальным состоянием
  9. Соединяем Redux и React
    1. Провайдер (Provider)
    2. Подключение
    3. Контейнер и компонент Presenter
  10. SoundCloud App
    1. Регистрация
    2. React Router
    3. Aвторизация
    4. Redux Thunk
    5. Set Me
    6. Получение треков
  11. SoundCloud Player
    1. Новый Redux Cycle
    2. Слушаем музыку!
  12. Заключительное слово

Проект с нуля

Я должен сказать, что многому научился когда реализовывал проект с нуля. А это делает разработку с  нуля совершенно осмысленной. Вы узнаете тонну нового материла не только о React + Redux, но и о JavaScript в целом, об его окружении. В этом руководстве мы будем учиться в процессе работы, стараться понять каждый шаг, так же, как это было со мной, когда я делал этот проект, но только с некоторыми полезными объяснениями. По окончании вы сможете создать собственный сторонний React + Redux проект.

Зависимости

Инструменты, которые я использовал в ходе реализации FaveSound:

  • Терминал (например, iTerm);
  • Текстовый редактор (например, Sublime Text);
  • Node и NPM;

В связи с тем, что зависимости в проекте могут измениться, ниже предоставлен список зависимостей, которые вам придётся установить в этом руководстве. Пожалуйста сообщите, если в будущем появятся значительные изменения.

package.json

Давайте начнём

Примечание: В последнее время, команда React’а выпустила более лёгкий и официально поддерживаемый способ создания приложений с React, Webpack и Babel. Вы можете использовать инструмент Create React App чтобы создать проект без конфигурации. Тем не менее, следующие главы этой статьи расскажут вам, как настроить Webpack и Babel в вашем проекте с нуля. 

Цель этой главы заключается в установке проекта. Мы создаём новую папку и инициализируем в ней новый npm проект.

Введите в ваш терминал следующие:

Последняя команда должна создать package.json файл, в котором будет находится список установленных пакетов и скрипты для сборки приложения.

Сейчас мы создадим папку dist, которая в дальнейшем будет обслуживать наше одностраничное приложение (SPA — Singe Page Application). Всё наше SPA приложение будет состоять из двух файлов: .html и .js. Пока что мы можем создать наш .html файл, который будет служить точкой входа для нашего приложения, а файл .js будет генерироваться позже, из всех наших исходных файлов (через Webpack).

Примечание: Папка dist нужна для того, чтобы в дальнейшем мы могли опубликовать наше приложение на хостинге, так, как я это сделал с FaveSound.

Из корневой папки проекта выполните следующие команды:

dist/index.html

Два важных замечания:

  • Файл bundle.js будет создан через Webpack автоматически.
  • Атрибут id=»app» поможет нашему корневому React компоненту найти свою точку входа.

Возможные, дальнейшие шаги в этом руководстве:

  1. Установить Webpack и объединить наши исходные файлы в один файл bundle.js;
  2. Разработать наш первый, корневой React компонент, который будет использовать нашу точку входа id=»app»;

Давайте пойдём по порядку.

Настройка Webpack

Мы будем использовать WebPack в качестве инструмента для сборки. Кроме этого, мы будем использовать webpack-dev-server, для обслуживания уже собранного приложения в нашей локальной среде.

Из корневой папки проекта выполните следующие команды:

Иногда, в дальнейшем я буду ссылаться на структуру папок, чтобы лучше понимать структуру нашего приложения.

Структура проекта:

В файл package.json нам нужно добавить скрипт запуска, в дополнение к скриптам по умолчанию, чтобы запускать наш webpack-dev-server.

package.json

Мы указали, что хотим использовать webpack-dev-server с некоторыми базовыми параметрами и файлом конфигурации webpack.config.js.

Теперь нужно создать этот файл конфигурации.

Из корневой проекта:

webpack.config.js

Грубо говоря, этот файл конфигурации описывает то, что (1) мы хотим использовать файл src/index.js в качестве точки входа для объединения всех наших файлов. (2) Сборка проекта в результате приведёт к файлу bundle.js, который будет создан в папке /dist. (3) Папка /dist будет использоваться для обслуживания нашего приложения.

В нашем проекте не хватает файла src/index.js. Нужно его создать.

Из корневой папки:

src/index.js

Структура проекта:

Теперь мы можем запустить наш webpack-dev-server, открыть наше приложение в браузере  (по умолчанию localhost:8080) и посмотреть на вывод в Developer Console.

Из корневой папки запустите команду:

Наше приложение обслуживается через Webpack! Мы собираем наши файлы через точку входа src/index.js в bundle.js и подключаем его в dist/index.html, после чего мы можем видеть уже собранный вывод в Developer Console.

Hot reloading

Огромное ускорение в разработке нам даст react-hot-loader, который сократит время обратной связи между сервером и браузером. В двух словах, каждый раз, когда мы что-то изменим в нашем исходном коде, все изменения отобразятся в браузере без перезагрузки страницы.

Перейдите в корневую папку проекта и выполните следующую команду:

webpack.config.js

Опять же из корневой папки проекта:

В этот раз вы также увидите вывод информации в консоль, но на этот раз её будет больше, так как там будет присутствовать информация о hot reloading’е. Мы почти закончили подготовку к написанию нашего первого React компонента, но всё-таки ещё одного кирпичика не хватает.

Babel

Babel позволяет писать наш код на ES6 (ES2015). В дальнейшем код будет скомпилирован в ES5, чтобы браузеры, у которых нет поддержки ES6, могли нормально его интерпретировать.

Кроме того, мы хотим использовать некоторые экспериментальные особенности ES6 (например, object spread), которые могут быть активированы с помощью stages.

Ну и в конце концов, так как мы используем React, нам нужна ещё одна конфигурация, чтобы конвертировать родные для React .jsx файлы в .js.

Из корневой папки проекта:

Теперь нам необходимо подкорректировать package.json и webpack.config.js файлы и внести изменения, связанные с Babel. Эти изменения включают в себя все выше перечисленные функции.

package.json

webpack.config.js

Запущенный npm скрипт прямо сейчас нужно приостановить, так как наше приложение ещё ничего не знает о React. Но теперь мы готовы написать наш первый React компонент, поэтому давайте начнём.

Примечание переводчика: В связи с обновлением некоторых пакетов, после того, как вы проделаете эти действия, скорее всего у вас в консоли появится сообщение об ошибке:

ERROR in ./src/index.js
Module build failed: Error: React Hot Loader: The Webpack loader is now exported separately. If you use Babel, we recommend that you remove «react-hot-loader» from the «loaders» section of your Webpack configuration altogether, and instead add «react-hot-loader/babel» to the «plugins» section of your .babelrc file. If you prefer not to use Babel, replace «react-hot-loader» or «react-hot» with «react-hot-loader/webpack» in the «loaders» section of your Webpack configuration.

Чтобы его исправить, вам нужно заменить loader в файле webpack.config.js на babel-loader. Чтобы получилось следующее:

И в файле package.json нужно добавить плагин в секцию babel.

Первый React Component

После установки react и react-dom наша сборка должна работать так же, как и до этого.

В нашем src/index.js мы можем реализовать нашу первую зацепку в мире React и избавиться от скучного вывода в консоль.

src/index.js

С помощью функции ReactDOM.render мы получаем элемент с id «app» из нашего файла index.html. Наш первый компонент Stream будет отображён в этом месте. К тому же, мы предоставляем нашему компоненту Stream жёстко зашитый в коде массив неизменяемых объектов Track (что логично, так как мы собираемся написать клиент для SoundCloud). Компонент Stream сам по себе будет показывать список треков. Но пока что у нас нет компонента Stream, так что давайте его реализуем.

Из папки src:

У нашей папки src начинает появляться структура. Мы организовываем наши файлы, технически разделяя их. Начнём с папки components, но позже добавим несколько папок.

Давайте изменим наш недавно созданный файл.

src/components/Stream.js

Наш компонент Stream представляет из себя функциональный компонент без состояния. Он получает массив tracks в аргументах, который по умолчанию является пустым массивом (это возможно благодаря ES6). Метод map оборачивает массив tracks и возвращает div’ы с track.title в качестве содержимого. Называется он функциональным компонентом без состояния (stateless functional component), потому что он только получает данные на вход и формирует данные на выходе. Компонент Stream ничего не знает о состоянии всего приложения (stateless). Это всего лишь функция, которая получает данные на вход и формирует данные на выходе — если быть более конкретными: у нас есть функция, которая получает состояние, а возвращает представление: (State) => View.

Структура проекта:

Когда вы запустите ваше приложение снова, в браузере вы должны увидеть два заголовка треков. Кроме этого, информация выведенная в консоль, даст подсказку о том, что параметр key отсутствует. React компонентам необходим параметр key чтобы однозначно идентифицировать себя в списке компонентов. Давайте это исправим, сохраним файл и посмотрим как подключится hot reloading и перезагрузит страницу.

src/components/Stream.js

Готово. Мы написали наш первый React код.

Мы сделали уже много всего предыдущих главах. Давайте обобщим это в краткие заметки:

  • Мы используем webpack + webpack-dev-server для сборки и обслуживания нашего приложения;
  • Мы используем Babel:
    1. Чтобы писать в стиле ES6;
    2. Чтобы у нас были .js файлы, а не .jsx;
  • src/index.js файл используется Webpack’ом как точка входа для сборки всего, что подключается через import, в один файл bundle.js;
  • bundle.js используется в dist/index.html;
  • dist/index.html предоставляет нам идентификатор в качестве точки входа для нашего корневого React компонента;
  • Мы создали нашу первую React зацепку через атрибут id в файле src/index.js;
  • Мы реализовали наш первый дочерний компонент как функциональный компонент без состояния в src/components/Stream.js;

Настройка тестов

Я хочу показать вам простую настройку тестов для ваших React компонентов. Мы будем тестировать компонент Stream.

Мы будем использовать mocha в качестве фреймворка тестирования, chai как asserts библиотеку и jsdom для предоставления нам чистой реализации JavaScript DOM, которая работает в Node.

Из корневой папки:

Ещё нам нужен файл конфигурации для дополнительной настройки тестов, особенного для нашего Virtual DOM.

Из корневой папки:

test/setup.js

По сути, мы глобально раскрываем созданный jsdom’ом документ и объект window, которые могут быть использованы React’ом во время тестов. Потом, мы раскрываем все свойства объекта window, чтобы в дальнейшем наши запущенные тесты могли использовать их. Последнее, но не менее важное, мы даём глобальный доступ к объектам React и expect. Это позволит нам не импортировать их в каждом из наших тестов.

В package.json нам нужно добавить ещё один скрипт для запуска наших тестов, который поддерживает Babel, использует mocha, использует наш ранее написанный test/setup.js файл и обходит все наши файлы в папке src с суффиксом spec.js.

В дополнение, существуют более аккуратные библиотеки, которые могут помочь нам с тестами React компонентов. Enzyme от Airbnb — это на данный момент статус-кво библиотека для тестирования React компонентов. Она основана на react-addons-test-utils и react-dom (последний мы уже установили через npm). Давайте установим оставшиеся.

Из корневой папки проекта:

Сейчас мы готовы написать наш первый тест для компонента.

src/components/Stream.spec.js

Здесь мы тестируем наш компонент Stream, массивом из двух треков. Как мы знаем, эти треки должны быть отображены. Утверждение expect проверяет, отобразились ли два элемента с классом .track. Если мы запустим наши тесты, они должны успешно пройти.

Из корневой директории:

Кроме того, мы можем усовершенствовать наш package.json с помощью скрипта test:watch.

При запуске сценария мы видим, что тесты выполняются при каждом внесении изменений в исходный код.

Из корневой директории:

Структура проекта:

Мы не будем больше писать тесты в этом руководстве, но один у нас уже есть. Не стесняйтесь добавлять тесты сами в последующих главах.

Redux

Redux позиционирует себя как предсказуемый контейнер состояний для JavaScript приложений. Большую часть времени, в клиентских приложениях, вы будете наблюдать Redux в связке с React. На самом деле он может гораздо больше. Как сам JavaScript используется на стороне сервера или в IoT приложениях, так и Redux может использоваться везде где нужен предсказуемый контейнер состояний. Дальше вы увидите, что Redux не связан строго с React, так как у него есть свой отдельный модуль, в то же время вы можете установить любой другой модуль и подключить его к React. Также существуют модули чтобы подключать Redux к других платформам. Кроме этого, экосистема вокруг Redux просто огромна. Как только вы погрузитесь в неё, вы узнаете много новых вещей. В целом, это не просто ещё одна библиотека. Вам нужно заглянуть во внутрь чтобы понять, какую именно проблему он будет решать для вас. Только в этом случае нужно его использовать! Если он не решает ваших проблем, просто не используйте его.

На данный момент, я хочу сказать большое спасибо Dan Abramov, создателю Redux, который не только предоставил нам простой и продуманный способ контролировать наши состояния, но ещё и показывает нам как делать огромный вклад в OpenSource ежедневно. Посмотрите его доклад с React Europe 2016, где он рассказывает о том какой путь прошёл Redux и что сделало его успешным.

Redux Cycle

Я называю это Redux Cycle, потому что он позволяет использовать однонаправленный поток данных. Redux Cycle вырос из flux архитектуры. В общем, вы вызываете действие в компоненте, это может быть кнопкой или что-то ещё. Кто-то это действие слушает, использует его данные, и создаёт новый глобальный объект состояния, который может быть предоставлен всем компонентам. Компоненты могут обновлять цикл и завершаться.

Давайте приступим к реализации нашего первого цикла в Redux!

Из корня проекта:

Отправка действий (Dispatching an Action)

Давайте отправим наше первое действие и объясним некоторые вещи.

src/index.js

Как вы можете видеть, мы инициализировали объект store с некоторыми функциями, которые мы ещё не объявили. Store — это экземпляр Redux объекта и хранилище нашего глобального состояния. К тому же, можно использовать лёгкий Store API чтобы отправлять действия, получать состояние или подписываться на события при изменениях.

В нашем случае, мы отправляем первое действие с нашими жёстко зашитыми в коде треками. Так как мы хотим подключить компонент Stream к хранилищу позже, нам не нужно передавать треки в качестве параметров (атрибутов) компонента.

Что мы будем делать дальше? В принципе, мы можем написать нашу функцию configureStore, которая создаёт объект store, или же мы можем посмотреть на наше первое отправленное действие. Продолжим с последнего, объясним что такое действия и создатели действий (action creators), потом перейдём к редьюсерам (reducers), которые будут заниматься (изменять) глобальным объектом состояния, ну и в конце мы создадим хранилище, который собственно и будет хранить глобальный объект состояния. После этого наш компонент сможет подписаться на хранилище, чтобы получать обновления или использовать интерфейс для отправки новых действий при изменении состояния.

Константы типов действий (Constant Action Types)

На ранних этапах Redux проектов, вы часто будете останавливаться на константах для определения ваших действий (экшенов). Эти константы становятся доступными для действий и редьюсеров. Вообще, хороший подход когда константы действий, которые описывают изменения вашего глобального состояния, находятся в одном месте.

Примечание: Если ваш проект разрастается , существуют и другие способы организации папок/файлов, в Redux проектах. 

Из папки src, выполните следующие команды:

src/constants/actionTypes.js

Создатели действий (Action Creators)

Теперь мы перейдём к шаблону создателей действий. Создатели действий возвращают объект с типом и данными. Тип — это константа, такая же как та которую мы объявляли в предыдущей главе. Данные могут быть чем угодно, что будет изменять глобальное состояние.

Из папки src, выполните следующие команды:

src/actions/track.js

Наш первый создатель действий принимает в качестве параметров треки, которые мы хотим установить в глобальное состояние и возвращает тип действия и данные.

Для того чтобы сохранить нашу аккуратную структуру, нам нужно установить точку входа для наших создателей действий через файл index.js.

Из папки actions:

src/actions/index.js

В этом файле мы будем собирать все наши создатели действий и экспортировать их как открытый интерфейс в остальную часть приложения. Всякий раз, когда нам нужно получить доступ к какому-то создателю действий откуда-то ещё, у нас для этого есть чётко определённый интерфейс. Не надо обращаться непосредственно к самим файлам создателей действий.

Редьюсеры (Reducers)

После того как мы отправили наше первое действие и написали наш первый создатель действий, кто-то должен быть в курсе того, что типы действий имеют доступы к глобальному состоянию. Эти функции называются редьюсерами, потому что они принимают действие с типом и данными, и приводят (reduce) его к новому состоянию (previousState, action) => newState.

Важно: Вместо того чтобы изменять previousState, мы возвращаем новый объект newState, так как состояние — неизменяемое.

Вы никогда не будете изменять previousState и всегда будете возвращать новый объект newState. Вам нужно сохранять структуру данных неизменной, чтобы избежать каких-либо побочных эффектов в приложении.

Давайте создадим наш первый редьюсер.

Из папки src:

src/reducers/track.js

Как вы можете видеть из примера выше, мы экспортируем анонимную функцию, редьюсер, как интерфейс к нашему уже существующему приложению. Редьюсер получает состояние и действие, как это было описано выше. Кроме того, мы можем определить параметр по умолчанию, благодаря ES6, в нашем случае это пустой массив.

Сам редьюсер содержит в себе конструкцию switch case, чтобы разделять типы действий. Сейчас он содержит в себе только один тип, но эта конструкция будет расти в соответствии с появлением новых типов в нашем приложении.

В конце концов, мы используем ES6 оператор spread (Примечание переводчика: в коде он выглядит как многоточие «…») чтобы сложить наше предыдущее состояние и переданные параметром треки в новый возвращаемый объект. Мы используем оператор spread, чтобы наш объект оставался неизменяемым. Я могу посоветовать библиотеку Immutable.js для обеспечения неизменных структур данных, но для простоты я буду использовать чистый синтаксис ES6.

Опять же чтобы сохранить аккуратную структуру нашего приложения, мы создадим точку входа для наших редьюсеров.

Из директории reducers:

src/reducers/index.js

Чтобы спасти нас от рефакторинга, я уже использую здесь функцию combineReducers. Обычно, нужно экспортировать один простой редьюсер и он будет возвращать всё состояние. При использовании combineReducers, у вас может быть несколько редьюсеров и каждый из них будет возвращать подсостояние (substate). Без combineReducers вы могли бы получить треки через глобальное состояние вот так:

Но с combineReducers у вас появится промежуточный слой чтобы добраться до подмножества состояний, создаваемый различными редьюсерами. В этом случае:

где track это подсостояние для обработки всех состояний треков в будущем.

Store c глобальным состоянием

Сейчас мы уже отправили наше первое действие, создали пару типов действий и создатель действий, и генерировали новое состояние с помощью редьюсера. Чего-то не хватает в нашем хранилище, который мы уже создали без некоторых ещё нереализованных функций в src/index.js.

Помните когда мы отправили наше первое действие через интерфейс хранилища store.dispatch(actionCreator(payload))? Таким образом хранилище в курсе наших редьюсеров и манипуляций со состоянием.

Давайте создадим файл хранилища.

Из папки src.

src/stores/configureStore.js

Redux предоставляет нам функцию createStore.  В конце концов, нам остаётся только вернуть созданное через combineReducers хранилище store = createStore(rootReducer, initialState = []).

В нашем файле происходит немного больше, чем просто возвращение готового Redux приложения. Redux хранилище знает о middleware, который может быть использован для того, чтобы что-то сделать между отправкой действия и моментом когда когда будет достигнут редьюсер. Есть куча middleware для Redux, но мы для начала будем использовать только logger middleware. logger middleware выводит в консоль информацию о каждом действии:  previousState, само действие и  nextState. Это помогает следить за состоянием приложения.

Ну и в конце мы используем вспомогательную функцию applyMiddleware чтобы связать наше хранилище с middleware, который мы указали.

Давайте запустим наше приложение и посмотрим, что получилось.

Из корня проекта:

В браузере мы не увидим треков из нашего глобального состояния, потому что не передаём глобального состояние в наш компонент Stream. Но в консоли мы можем увидеть наше первое действие, которое было отправлено.

Давайте соединим наш компонент Stream с наших Redux контейнером, чтобы завершить Redux cycle.

Соединяем Redux и React

Как я упоминал в самом начале, есть библиотеки которые являются связующем звеном между Redux и другими библиотеками. Так как мы используем React, мы хотим подключить Redux к нашим React компонентам.

Вы помните я рассказывал о лёгком Store API в Redux? Мы никогда не будем наслаждаться функциональностью store.subscribe для прослушивания изменений хранилища. С react-redux мы пропускаем этот шаг и позволяем этой библиотеке позаботиться о подключении компонентов в хранилищу для прослушивания изменений.

По сути нам надо выполнить всего два шага, чтобы связать наше Redux хранилище с React компонентами.

Провайдер (Provider)

Провайдер из react-redux помогает нам сделать хранилище и его функциональные возможности доступными во всех его дочерних компонентах. Единственное, что мы должны сделать, это инициализировать наше хранилище и обернуть наши дочерние компоненты в компонент Provider. В конце компонент Provider использует хранилище как свойство.

src/index.js

Сейчас мы сделали Redux хранилище доступным для всех дочерних компонентов, в нашем случае для компонента Stream.

Подключение

Подключение функциональных возможностей из react-redux помогает нам связать компоненты, которые встроены в вспомогательный компонент Provider, с Redux хранилищем. Мы можем расширить наш компонент Stream следующим образом, чтобы получить требуемое состояние из Redux хранилища.

Помните, когда у нас были жёстко зашитые в коде треки напрямую передаваемые в компонент Stream? Сейчас мы устанавливаем эти треки через Redux cycle в нашем глобальном состоянии и хотим получать часть этого состояния в компоненте Stream.

src/components/Stream.js

Как вы можете видеть, сам компонент не изменился вообще.

Если в двух словах то, мы используем функцию connect чтобы получить наш компонент Stream в качестве параметра для возвращаемого компонента высшего порядка. У компонента высшего порядка есть доступ к Redux хранилищу, в том время как компонент Stream может только представлять наши данные.

Кроме того, функция connect в качестве первого аргумента принимает функцию mapStateToProps, которая возвращает объект. Объект представляет собой часть нашего глобального состояния. В mapStateToProps мы раскрываем только ту часть глобального состояния, которая необходима компоненту.

К тому же стоит отметить, что у нас до сих пор есть доступ к свойствам из родительских компонентов с помощью <Stream something={thing} /> через функцию mapStateToProps. Функции предоставляют нам вторым аргументом свойства, которые мы можем передать с подсостоянием самому компоненту Stream.

Теперь запустите приложение и на этот раз вы должны увидеть отображённый список треков в вашем браузере. Мы уже видели эти треки на предыдущем шаге, но теперь мы достали их из нашего Redux хранилища.

Тест должен провалиться, но мы исправим это на следующем шаге.

Контейнер и компонент Presenter

Сейчас у нашего компонента Stream есть две обязанности. Во-первых он подключает какое-то состояние к нашему компоненту, а во-вторых он отображает некоторый DOM. Мы можем разделить это на две части, контейнер и ведущий (presenter) компонент. Контейнер будет отвечать за подключение компонента к миру Redux, а ведущий компонент будет отображать некоторый DOM.

Давайте перепишем!

Для начала нам нужно организовать нашу папку. Так как в конечном итоге для компонента Stream у нас будет не один файл, нам нужно создать специальную папку Stream, в которой будут находится все файлы компонента.

Из папки components:

Папка Stream содержит в себе index.js файл (контейнер), presenter.js (ведущий компонент) и spec.js файл (для тестов). В дальнейшем туда можно будет положить style.css/less/scss, story.js и т.д файлы.

Давайте перепишем все файлы. Так как появятся новые строки кода, я выделил важные новые, важные части, которые придут с этим рефакторингом. Большая часть старого кода будет просто разделена по файлам.

src/components/Stream/index.js

src/components/Stream/presenter.js

src/components/Stream/spec.js

Сейчас вы можете удалить старые файлы Stream.js и Stream.spec.js, так как они были переработаны и теперь находятся в папке Stream.

После запуска приложения вы до сих пор должны видеть список треков.

В последних шагах мы завершили Redux Cycle и подключили наши компоненты к среде Redux. Сейчас давайте погрузимся в разработку реального приложения — клиента SoundCloud.

SoundCloud App

Нет ничего лучше чем разработка настоящего приложения основанного на реальных данных. Вместо того чтобы жёстко зашивать данные для отображения в коде, лучше испытать потрясающее чувство и достать данные из хорошо известного сервиса SoundCloud.

В этой главе мы будем реализовывать наш клиент для SoundCloud, а это означает что мы будем авторизовываться как пользователь и выводить список последних треков из потока. Кроме того мы сможем воспроизводить их.

Регистрация

Перед тем как мы сможем разработать клиент для SoundCloud, нам необходимо иметь учётную запись на SoundCloud и зарегистрировать приложение. Зайдите в раздел разработчиков Developers SoundCloud и нажмите на ссылку «Register a new app».  Придумайте имя для приложения и зарегистрируйте его.  (Примечание переводчика: На самом деле сейчас нужно заполнить немного больше полей и выглядит всё по другому, но там всё интуитивно понятно)

На последнем шаге регистрации вам необходимо предоставить «Redirect URI» своего приложения, чтобы можно было регистрироваться и авторизовываться позже через всплывающую форму входа. Так как мы разрабатываем локально, то в качестве Redirect URI мы установим «http://localhost:8080/callback».

На предыдущем шаге мы получили две константы, которые мы должны использовать в нашем приложении: Client ID и Redirect URI. Нам необходимы обе константы чтобы настроить процесс авторизации. Перенесём эти константы в файл.

В папке constants:

src/constants/auth.js

Теперь мы можем авторизовываться с помощью SoundCloud:

src/index.js

React Router

В нашем приложении процесс авторизации будет опираться на маршрут «/callback». Поэтому нам надо установить React Router, чтобы обеспечить наше приложение простой маршрутизацией.

Из корня проекта:

Теперь нам нужно добавить следующую строчку в файл конфигурации Webpack.

webpack.config.js

historyApiFallback позволяет делать нашему приложению чистую маршрутизацию на стороне клиента. Как правило изменение маршрута приводит к запросу к серверу для получения новых ресурсов.

Давайте напишем два роута: один для нашего приложения, другой для обратного вызова и обработки авторизации. Поэтому мы используем некоторые вспомогательные компоненты предоставляемые react-router. В целом, нам нужно указать соответствие путей и компонентов. Поэтому мы определили, что будем видеть компонент Stream на корневом пути «/» и компонент обратного вызова на пути «/callback» (где происходит авторизация). Кроме того, мы можем указать обёртку для компонента, такую как App. В его реализации мы увидим почему это хорошо. Также мы используем react-router-redux для синхронизации истории браузера с хранилищем. Это поможет нам оперативно реагировать на изменения роутеров.

src/index.js

В конце у нас есть два новых компонента: App как обёртка и Callback для авторизации. Давайте создадим первый из них.

Из папки components:

src/components/App/index.js

Здесь приложение почти ничего не делает, но оно передаётся всем дочерним компонентам. Мы больше не будем использовать этот компонент в данном руководстве, но в будущих реализациях вы бы могли его использовать для статичного Header, Footer, Playlist или Player компонентов, тогда как дочерние будут изменяться.

Давайте создадим наш Callback компонент.

В папке components, выполните следующие команды.

src/components/Calback/index.js

Это стандартная реализация создания обратного вызова для API SoundCloud. В дальнейшем мы больше не будем трогать этот файл.

Последний шаг настройки маршрутизатора (Router’а), это предоставление нашему хранилищу состояния маршрутизатора при навигации по страницам.

src/reducers/index.js

src/stores/configureStore.js

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

Авторизация

Давайте авторизуемся через SoundCloud! Нам создать новое действие, чтобы вызвать событие авторизации. Давайте раскроем функцию auth и затем создадим требуемый для действия файл.

src/actions/index.js

Из папки actions:

src/actions/auth.js

Мы можем подключится к SoundCloud API, войти на сайт с нашими учётными данными и увидеть в консоли детали о нашем аккаунте.

Никто не вызывал это действие, так что давайте сделаем это в нашем компоненте Stream, для простоты.

В нашем компоненте контейнера мы лишь сопоставили некоторые состояния с нашими ведущими (presenter) компонентами. Теперь речь идёт о второй функции, в которую мы можем передать соединяющую функцию mapDispatchToProps. Эта функция поможет нам передать действия нашему ведущему компоненту. В mapDispatchToProps мы возвращаем объект с функциями, в нашем случае с одной функцией onAuth, и используем ранее созданное действие auth. Кроме этого нам нужно связать создатель действий с функцией отправки.

Давайте сейчас используем новое доступное действие в нашем ведущем компоненте.

src/components/Stream/presenter.js

Мы просто создали кнопку и предали ей в обработчик onClick функцию onAuth. После того как мы запустим наше приложение снова и кликнем на кнопке входа, мы должны увидеть информацию о текущем пользователе в консоли. В дополнении мы ещё увидим некоторое сообщение об ошибке, потому что наши действия уходят в никуда, так как мы не сопоставили для него редьюсер.

Примечание: Возможно нам потребуется установка полифилла для fetch, так как некоторые браузеры ещё не поддерживают fetch API.

Из корня проекта:

webpack.config.js

Redux Thunk

Мы можем увидеть объект текущего пользователя в консоле, но мы его ещё не храним. Ещё мы используем наше первое асинхронное действие, потому что мы должны дождаться пока сервер SoundCloud ответит на наш запрос. Redux среда предоставляет несколько middleware для решения проблем с асинхронными действиями (смотрите список ниже). Одним из них является redux-thunk. thunk возвращает нам функцию вместо действия. Так как мы имеем дело с асинхронными вызовами, мы можем задерживать функцию отправки с помощью middleware. Кроме того, внутренняя функция даёт нам доступ к хранилищу функций отправки и getState.

Примечание: Building React Applications with Idiomatic Redux от egghead.io и Dan Abramov как реализовать ваш собственный thunk middleware. 

Некоторые сторонние middleware в Redux:

Из корня проекта:

Давайте добавим thunk middleware к нашему хранилище.

src/stores/configurationStore.js

Set Me

Сейчас у нас всё готово, чтобы сохранить объект пользователя в хранилище. Поэтому нам нужно создать новый набор данных, создателя действий и редьюсер.

src/constants/actionTypes.js

src/actions/auth.js

Вместо того чтобы выводить информацию об извлечённом объекте пользователя в консоль, мы просто вызываем наш создатель действий. Так же мы можем видеть, что thunk middleware требует нас вернуть функцию вместо объекта. Функция предоставляет нам доступ к функциональным возможностям хранилища, связанными с отправкой действий.

Давайте добавим новый редьюсер.

src/reducers/index.js

Из папки reducers.

src/reducers/auth.js

Редьюсер учитывает новый тип действия и возвращает newState с нашим пользователем.

Сейчас мы хотим визуально видеть, в нашем DOM, зашёл ли пользователь на сайт. Поэтому мы можем заменять кнопку входа сразу после того как пользователь авторизовался.

src/components/Stream/index.js

В нашем компоненте контейнера мы сопоставили новое состояния, текущего пользователя, с нашим ведущим компонентом.

src/components/Stream/presenter.js

Ведущий компонент решает, что нужно показывать, имя пользователя или кнопку входа. Когда мы снова запустим наше приложение, после того как мы авторизуемся, вместо кнопки входа должно появиться имя пользователя.

Из корня проекта.

Получение треков

Сейчас мы уже авторизованы на SoundCloud сервере. Поэтому давайте достанем настоящие треки и заменим те, что жёстко зашиты коде.

src/index.js

Здесь, мы только убрали жёстко зашитые в коде треки. И ещё мы больше не отправляем действия чтобы установить какое-то начальное состояние.

src/actions/auth.js

После авторизации мы просто отправляем новый асинхронный запрос чтобы получить данные по треку из SoundCloud API. Так как у нас уже был создатель действия чтобы установить треки в наше состояние, мы можем использовать его повторно.

Примечание: В возвращаемых данных содержится не только список треков, также содержатся некоторые метаданные, которые могут быть использованы в дальнейшем для извлечения данных с разбивкой на страницы. Вам придётся сохранить свойство next_href  для этого. 

Структура данных SoundCloud выглядит несколько иначе, чем наши жёстко зашитые треки. Нам нужно поправить ведущий компонент Stream.

src/components/Stream/presenter.js

Ещё нам нужно поправить наш тест чтобы он соблюдал новую структуру.

src/components/Stream/spec.js

После того как вы запустите приложение и авторизуетесь в нём, вы должны увидеть некоторые треки из вашего личного канала.

Примечание: Я надеюсь, что треки отобразятся, даже если у вас новый аккаунт на SoundCloud. Если вы получаете какие-то пустые данные, вам нужно использовать непосредственно сам SoundCloud, чтобы добавить треки.

Из корня проекта:

SoundCloud Player

Насколько круто было бы иметь аудио-плеер в браузере? Поэтому наш последний шаг заключается в том, чтобы сделать треки воспроизводимыми.

Новый Redux Cycle

Вы уже знакомы с процедурой создания действий, создателей действий и редьюсеров. А ещё вы должны уметь вызывать это из компонента. Давайте начнём с того, что предоставим нашему компоненту Stream, ещё не существующую функциональность onPlay. Кроме того, мы отобразим кнопку Play возле каждого трека, которая будет запускать эту функциональность.

src/components/Stream/presenter.js

В контейнере компонента Stream мы можем сопоставить это действие с ведущим компонентом.

src/components/Stream/index.js

Теперь мы должны реализовать несуществующий создатель действий  playTrack.

src/actions/index.js

src/actions/track.js

Не забудьте экспортировать новый тип действия как константу.

В нашем редьюсере мы оставляем место для другого начального состояния. Изначально не будет установлено никакого активного трека, но после того как мы вызовем триггер проигрывания трека, он будет установлен как activeTrack.

src/reducers/track.js

Ещё мы хотим показать воспроизведение текущего трека, поэтому нам необходимо сопоставить activeTrack в нашем контейнере компонента Stream.

src/components/Stream/index.js

После запуска приложения мы должны авторизоваться чтобы увидеть список треков. redux-logger должен показать некоторую информацию в консоли, которую мы должны установить как activeTrack. Но музыки ещё нет! Давайте реализуем это.

Cлушаем музыку!

На предыдущем шаге мы уже передали activeTrack нашему ведущему компоненту Stream. Давайте посмотрим, что мы можем сделать с этим.

src/components/Stream/presenter.js

Нам нужен CLIENT_ID для авторизации аудио-плеера в SoundCloud API, чтобы получить поток трека через его stream_url. В React 15 вы можете вернуть null, если нет activeTrack. В более старых версиях вы должны вернуть <noscript />.

Когда мы запустим наше приложение и попытаемся воспроизвести трек, то вывод в консоле скажет, что мы не можем определять ссылки на функциональные компоненты без состояния. Но нам нужна ссылка на аудио элемент с возможностью использовать его Audio API. Давайте преобразуем ведущий компонент Stream к компоненту с состоянием. Мы увидим, что это даёт нам контроль над аудио элементом.

src/components/Stream/presenter.js

Давайте снова запустим наше приложение. Авторизуемся и увидим список треков. Теперь мы можем нажать на кнопочку и слушать музычку.

Заключительное слово

Надеюсь, что вам понравился урок и вы узнали много нового также как и я. Я не планировал писать так много, но я надеюсь что до конца дойдёт достаточно много людей.

Я должен поблагодарить таких людей как Christopher Chedeau и Dan Abramov, которые побуждают людей вроде меня делать вклад в OpenSource, в ходе разговоров.

Я открыт для обратной связи и сообщений об ошибках в этой статье. Вы можете писать в комментарии под статьёй или выйти на меня через Twitter.

Вы можете ещё раз взглянуть на favesound-redux. Не стесняйтесь его использовать, вносить в него свой вклад, поднимать вопросы когда вы нашли ошибки, или же просто использовать его в качестве плана для своего собственного приложения.

Комментарии

Добавить комментарий

  • Slava Vasilenko

    Invalid configuration object. Webpack has been initialised using a configuration object that does not match the API schema.
    — configuration.resolve.extensions[0] should not be empty.

  • Slava Vasilenko

    не получается сделать по туториалу, два раза пробовал. С конфигом какйто fail. С гита идет.

    • ZapevalovAnton

      Может быть текст ошибки хотя бы напишите?

  • Дима Дихтярь

    Статья замечательная. У кого-то все получилось? 4 дня на регистрации в soundcloud вишу.
    Никто не желает заменить soundcloud чем-то более простым и выпустить свою редакцию? Было бы здорово.
    Спасибо за перевод. Качественно.

    • ZapevalovAnton

      Спасибо и вам за отзыв. Если нет проблем с английским, то могу посоветовать ещё статью в которой разрабатывается клон Yelp, правда, она очень длинная.
      https://www.fullstackreact.com/articles/react-tutorial-cloning-yelp/

      • Дима Дихтярь

        Благодарю.
        С soundcloud не могу победить исключение при нажатии на PLAY:
        «Uncaught (in promise) DOMException: Failed to load because no supported source was found.»
        Не подскажите от чего такое может быть?

  • Bang

    Спасибо за перевод!