Russian (Pусский) translation by Marat Amerov (you can also view the original English article)



В этом уроке я покажу вам, как мы можем использовать силу React и Phoenix для создания приложения ленты, которое будет обновляться в режиме реального времени, когда мы добавим новые фиды в нашу базу данных.
Введение
Elixir известен своей стабильностью и функциями реального времени, а Phoenix использует способность Erlang VM обрабатывать миллионы соединений наряду с красивым синтаксисом и производительным инструментарием Elixir. Это поможет нам в создании обновления данных в реальном времени через API, которые будут использоваться нашим приложением React для отображения данных в пользовательском интерфейсе.
Начало
У вас должны быть установлены Elixir, Erlang и Phoenix. Подробнее об этом можно узнать на веб-сайте Phoenix. Кроме того, мы будем использовать заготовку React boilerplate, поскольку она хорошо поддерживается и правильно документирована.
Подготовка API
В этом разделе мы загрузим наше приложение Phoenix API и добавим каналы для обновления API в режиме реального времени. Мы просто будем работать с фидом (он будет содержать заголовок и описание), и как только его значение будет изменено в базе данных, API отправит обновленное значение нашему фтонтенд приложению.
Создание приложения
Давайте сначала создадим приложение Phoenix.
mix phoenix.new realtime_feed_api --no-html --no-brunch
Это создаст скелет Phoenix приложения внутри папки с именем realtime_feed_api. Опция --no-html
не будет создавать все статические файлы (что полезно, если вы создаете приложение только для API), а опция --no-brunch
не будет включать сборщик статики Phoenix, Brunch. Убедитесь, что вы устанавливаете зависимости, когда это запрашивается.
Давайте зайдем в папку и создадим нашу базу данных.
cd realtime_feed_api
Нам нужно будет удалить поля username и password из нашего файла config/dev.exs, так как мы будем создавать нашу базу данных без какого-либо имени пользователя или пароля. Это для упрощения, чтобы все было просто для этого урока. Для вашего приложения, убедитесь, что вы сначала создайте базу данных с именем пользователя и паролем.
mix ecto.create
Вышеупомянутая команда создаст нашу базу данных. Теперь мы можем запустить наш Phoenix-сервер и проверить, все ли в порядке.
mix phoenix.server
Вышеупомянутая команда запускает наш Phoenix-сервер, и мы можем перейти к http://localhost:4000, чтобы увидеть, как он работает. В настоящее время он будет бросать ошибку no route found, поскольку мы еще не создали никаких маршрутов!
Не стесняйтесь проверять свои изменения с помощью моего коммита.
Добавление модели фида
На этом этапе мы добавим нашу модель Feed в наше приложение Phoenix. Модель Feeds будет состоять из title и description.
mix phoenix.gen.json Feed feeds title:string description:string
Вышеприведенная команда создаст нашу модель и контроллер Feed. Она также будет генерировать спецификации (которые мы не будем модифицировать в этом учебнике, просто для краткости).
Вам нужно добавить маршрут /feeds
в файл web/router.ex внутри скоупа api:
resources "/feeds", FeedController, except: [:new, :edit]
Нам также необходимо выполнить миграцию, чтобы создать таблицу feeds в нашей базе данных:
mix ecto.migrate
Теперь, если мы перейдем на http://localhost:4000/api/feeds, мы увидим, что API отправляет нам пустой ответ, поскольку в нашей таблице feeds нет данных.
Вы можете проверить мой коммит для справки.
Добавление канала фидов
На этом этапе мы добавим канал Feed в наше приложение Phoenix. Каналы предоставляют средства для двунаправленной связи от клиентов, которые интегрируются со слоем Phoenix.PubSub
для легкой функциональности в реальном времени.
mix phoenix.gen.channel feed
Вышеприведенная команда создаст файл feed_channel.ex внутри папки web/channels. Через этот файл наше приложение React будет обмениваться обновленными данными с базой данных с помощью сокетов.
Нам нужно добавить новый канал в наш файл web/channels/user_socket.ex:
channel "feeds", RealtimeFeedApi.FeedChannel
Поскольку мы не делаем аутентификацию для этого приложения, мы можем изменить наш файл web/channels/feed_channel.ex. Нам понадобится один метод join для нашего приложения React, чтобы присоединиться к нашему каналу фидов, один метод handle_out, чтобы отправлять полезную нагрузку через соединение сокета, и один метод broadcast_create, который будет транслировать полезную нагрузку всякий раз, когда в базе данных создается новый фид.
def join("feeds", payload, socket) do {:ok, "Joined feeds", socket} end
def handle_out(event, payload, socket) do push socket, event, payload {:noreply, socket} end
def broadcast_create(feed) do payload = %{ "id" => to_string(feed.id), "title" => feed.title, "description" => feed.description } RealtimeFeedApi.Endpoint.broadcast("feeds", "app/FeedsPage/HAS_NEW_FEEDS", payload) end
Эти три метода определены выше. В методе broadcast_create мы используем app/FeedsPage/HAS_NEW_FEEDS
, так как мы будем использовать это как константу для нашего контейнера состояний Redux, который будет отвечать за то, чтобы фронтенду было известно, что в базе данных есть новые фиды. Мы обсудим это, когда мы создадим наше фронтенд приложение.
В конце концов, нам нужно будет только вызвать метод broadcast_change через наш файл feed_controller.ex всякий раз, когда в наш метод create вставляются новые данные. Наш метод create будет выглядеть примерно так:
def create(conn, %{"feed" => feed_params}) do changeset = Feed.changeset(%Feed{}, feed_params) case Repo.insert(changeset) do {:ok, feed} -> RealtimeFeedApi.FeedChannel.broadcast_create(feed) conn |> put_status(:created) |> put_resp_header("location", feed_path(conn, :show, feed)) |> render("show.json", feed: feed) {:error, changeset} -> conn |> put_status(:unprocessable_entity) |> render(RealtimeFeedApi.ChangesetView, "error.json", changeset: changeset) end end
Метод create отвечает за вставку новых данных в базу данных. Вы можете проверить мой коммит для справки.
Добавление поддержки CORS для API
Нам нужно реализовать эту поддержку, поскольку в нашем случае API обслуживается с http://localhost:4000, но наше фронтенд приложение будет работать на http://localhost:3000. Добавление поддержки CORS очень просто. Нам просто нужно добавить cors_plug в наш файл mix.exs:
defp deps do [ ... {:cors_plug, "~> 1.3"} ] end
Теперь мы останавливаем наш сервер Phoenix с помощью Control-C и выбираем зависимость, используя следующую команду:
mix deps.get
Нам нужно добавить следующую строку в наш файл lib/realtime_feed_api/endpoint.ex:
plug CORSPlug
Вы можете проверить мой коммит. Мы выполнили все наши текущие изменения. Давайте теперь сосредоточимся на фронтенд приложении.
Обновление данных фронтенд в реальном времени
Как упоминалось ранее, мы будем использовать react-boilerplate, чтобы начать работу с нашим фронтенд приложением. Мы будем использовать саму Redux saga, который будет слушать наши отправленные действия, и на основе этого пользовательский интерфейс будет обновлять данные.
Поскольку в шаблоне уже настроено все, нам не нужно его настраивать. Тем не менее, мы будем использовать команды, доступные в шаблоне, чтобы подкрасить наше приложение. Давайте сначала клонируем репозиторий:
git clone
https://github.com/react-boilerplate/react-boilerplate.git
realtime_feed_ui
Создание приложения
Теперь нам нужно зайти в папку realtime_feed_ui и установить зависимости.
cd realtime_feed_ui && npm run setup
Это инициализирует новый проект с помощью этого шаблона, удаляет историю git react-boilerplate
, устанавливает зависимости и инициализирует новый репозиторий.
Теперь давайте удалим пример приложения, который предоставляется шаблоном, и заменим его наименьшим количеством кода, необходимого для начала написания нашего приложения:
npm run clean
Теперь мы можем запустить наше приложение, используя npm run start
и посмотреть, как он работает на http://localhost:3000/.
Вы можете ссылаться на мой коммит.
Добавление необходимых контейнеров
На этом этапе мы добавим два новых контейнера FeedsPage и AddFeedPage в наше приложение. Контейнер FeedsPage отобразит список фидов, а контейнер AddFeedPage позволит нам добавить новый фид в нашу базу данных. Для создания наших контейнеров мы будем использовать react-boilerplate.
npm run generate container
Вышеупомянутая команда используется для скаффолдинга в нашем приложении. После того, как вы наберете эту команду, он попросит имя компонента, которое в нашем случае будет FeedsPage, и мы будем использовать параметр Component на следующем шаге. Нам не понадобятся заголовки, но нам понадобятся actions/constants/selectors/reducer, а также sages для наших асинхронных потоков. Нам не нужны i18n messages для нашего приложения. Нам также необходимо будет придерживаться аналогичного подхода для создания нашего контейнера AddFeedPage.
Теперь у нас есть куча новых файлов для работы. Это экономит нам много времени. В противном случае нам придется создавать и настраивать все эти файлы самостоятельно. Кроме того, генератор создает тестовые файлы, которые очень полезны, но мы не будем писать тесты в этом уроке.
Давайте просто добавим наши контейнеры в наш файл routes.js:
{ path: '/feeds', name: 'feedsPage', getComponent(nextState, cb) { const importModules = Promise.all([ import('containers/FeedsPage/reducer'), import('containers/FeedsPage/sagas'), import('containers/FeedsPage'), ]); const renderRoute = loadModule(cb); importModules.then(([reducer, sagas, component]) => { injectReducer('feedsPage', reducer.default); injectSagas(sagas.default); renderRoute(component); }); importModules.catch(errorLoading); }, }
Это добавит наш контейнер FeedsPage в наш маршрут /feeds
. Мы можем проверить это, посетив http://localhost:3000/feeds. В настоящее время он будет полностью пустым, поскольку в наших контейнерах ничего нет, но в консоли нашего браузера ошибок не будет.
Мы сделаем то же самое для нашего контейнера AddFeedPage.
Вы можете ссылаться на мой коммит для всех изменений.
Создание листинговой страницы фидов
На этом этапе мы создадим FeedsPage, в котором будут перечислены все наши фиды. Чтобы сохранить этот учебник небольшим, мы не будем здесь добавлять какие-либо стили, но в конце нашего приложения я сделаю отдельный коммит, который добавит некоторые декорации в наше приложение.
Начнем с добавления наших констант в наш файл app/container/FeedsPage/constants.js:
export const FETCH_FEEDS_REQUEST = 'app/FeedsPage/FETCH_FEEDS_REQUEST'; export const FETCH_FEEDS_SUCCESS = 'app/FeedsPage/FETCH_FEEDS_SUCCESS'; export const FETCH_FEEDS_ERROR = 'app/FeedsPage/FETCH_FEEDS_ERROR'; export const HAS_NEW_FEEDS = 'app/FeedsPage/HAS_NEW_FEEDS';
Нам понадобятся эти четыре константы:
- Константа FETCH_FEEDS_REQUEST будет использоваться для инициализации запроса на выборку.
- Константа FETCH_FEEDS_SUCCESS будет использоваться, когда запрос на выборку будет успешным.
- Константа FETCH_FEEDS_ERROR будет использоваться, когда запрос на выборку не будет выполнен.
- Константа HAS_NEW_FEEDS будет использоваться, если в нашей базе данных есть новый фид.
Давайте добавим наши действия в наш файл app/container/FeedsPage/actions.js:
export const fetchFeedsRequest = () => ({ type: FETCH_FEEDS_REQUEST, }); export const fetchFeeds = (feeds) => ({ type: FETCH_FEEDS_SUCCESS, feeds, }); export const fetchFeedsError = (error) => ({ type: FETCH_FEEDS_ERROR, error, }); export const checkForNewFeeds = () => ({ type: HAS_NEW_FEEDS, });
Все эти действия не требуют объяснений. Теперь мы структурируем initialState нашего приложения и добавим редюсер в наш файл app/container/FeedsPage/reducer.js:
const initialState = fromJS({ feeds: { data: List(), ui: { loading: false, error: false, }, }, metadata: { hasNewFeeds: false, }, });
Это будет initialState нашего приложения (состояние перед загрузкой данных). Поскольку мы используем ImmutableJS, мы можем использовать его структуру данных List для хранения наших неизменных данных. Функция редюсера будет выглядеть примерно так:
function addFeedPageReducer(state = initialState, action) { switch (action.type) { case FETCH_FEEDS_REQUEST: return state .setIn(['feeds', 'ui', 'loading'], true) .setIn(['feeds', 'ui', 'error'], false); case FETCH_FEEDS_SUCCESS: return state .setIn(['feeds', 'data'], action.feeds.data) .setIn(['feeds', 'ui', 'loading'], false) .setIn(['metadata', 'hasNewFeeds'], false); case FETCH_FEEDS_ERROR: return state .setIn(['feeds', 'ui', 'error'], action.error) .setIn(['feeds', 'ui', 'loading'], false); case HAS_NEW_FEEDS: return state .setIn(['metadata', 'hasNewFeeds'], true); default: return state; } }
В принципе, то, что мы здесь делаем, меняет наше состояние на основе констант из наших действий. Таким образом, мы можем очень легко показать загрузчики и сообщения об ошибках. Это будет намного яснее, когда мы будем использовать это во фронтенде.
Пришло время создавать наши селекторы с помощью reselect, который является библиотекой селекторов для Redux. С помощью reselect мы можем легко извлекать значения сложного состояния. Давайте добавим следующие селекторs в наш app/containers/FeedsPage/selectors.js:
const feeds = () => createSelector( selectFeedsPageDomain(), (titleState) => titleState.get('feeds').get('data') ); const error = () => createSelector( selectFeedsPageDomain(), (errorState) => errorState.get('feeds').get('ui').get('error') ); const isLoading = () => createSelector( selectFeedsPageDomain(), (loadingState) => loadingState.get('feeds').get('ui').get('loading') ); const hasNewFeeds = () => createSelector( selectFeedsPageDomain(), (newFeedsState) => newFeedsState.get('metadata').get('hasNewFeeds') );
Как вы можете видеть здесь, мы используем структуру нашего initialState для извлечения данных из нашего состояния. Вам просто нужно запомнить синтаксис reselect.
Пришло время добавить наши sagas с использованием redux-sage. Здесь основная идея состоит в том, что нам нужно создать функцию для извлечения данных и другую функцию, чтобы наблюдать за начальной функцией, так что всякий раз, когда отправляется какое-либо конкретное действие, нам нужно вызвать начальную функцию. Давайте добавим функцию, которая будет извлекать наш список фидов из бекенд приложения в нашем файле app/container/FeedsPage/sagas.js:
function* getFeeds() { const requestURL = 'http://localhost:4000/api/feeds'; try { // Call our request helper (see 'utils/Request') const feeds = yield call(request, requestURL); yield put(fetchFeeds(feeds)); } catch (err) { yield put(fetchFeedsError(err)); } }
Здесь request - это просто функция-утилита, которая вызывает наш API бекенда. Весь файл доступен в react-boilerplate. Мы сделаем небольшое изменение в нем после заполнения нашего файла sagas.js.
Нам также нужно создать еще одну функцию для наблюдения функции getFeeds:
export function* watchGetFeeds() { const watcher = yield takeLatest(FETCH_FEEDS_REQUEST, getFeeds); // Suspend execution until location changes yield take(LOCATION_CHANGE); yield cancel(watcher); }
Как мы видим здесь, функция getFeeds вызывается, когда мы вызываем действие, содержащее константу FETCH_FEEDS_REQUEST.
Теперь давайте скопируем файл request.js из react-boilerplate в наше приложение внутрь папки app/utils, а затем изменим функцию request:
export default function request(url, method = 'GET', body) { return fetch(url, { headers: { 'Content-Type': 'application/json', }, method, body: JSON.stringify(body), }) .then(checkStatus) .then(parseJSON); }
Я только что добавил несколько значений по умолчанию, которые помогут нам сократить код позже, поскольку нам не нужно передавать метод и заголовки каждый раз. Теперь нам нужно создать другой файл util внутри папки app/utils. Мы назовем этот файл socketSagas.js. Он будет содержать четыре функции: connectToSocket, joinChannel, createSocketChannel и handleUpdatedData.
Функция connectToSocket будет отвечать за подключение к нашему бекенд API. Мы будем использовать npm пакет phoenix . Поэтому нам нужно будет установить его:
npm install phoenix --save
Это установит npm пакет phoenix и сохранит его в нашем файле package.json. Функция connectToSocket будет выглядеть примерно так:
export function* connectToSocket() { const socket = new Socket('ws:localhost:4000/socket'); socket.connect(); return socket; }
Затем мы определяем нашу функцию joinChannel, которая будет отвечать за присоединение определенного канала от бекенда. Функция joinChannel будет иметь следующее содержимое:
export function* joinChannel(socket, channelName) { const channel = socket.channel(channelName, {}); channel.join() .receive('ok', (resp) => { console.log('Joined successfully', resp); }) .receive('error', (resp) => { console.log('Unable to join', resp); }); return channel; }
Если присоединение будет успешным, мы будем логировать 'Joined successfully' только для тестирования. Если во время фазы соединения произошла ошибка, мы также будем регистрировать это только для целей отладки.
CreateSocketChannel будет отвечать за создание канала событий из данного сокета.
export const createSocketChannel = (channel, constant, fn) => // `eventChannel` takes a subscriber function // the subscriber function takes an `emit` argument to put messages onto the channel eventChannel((emit) => { const newDataHandler = (event) => { console.log(event); emit(fn(event)); }; channel.on(constant, newDataHandler); const unsubscribe = () => { channel.off(constant, newDataHandler); }; return unsubscribe; });
Эта функция также будет полезна, если мы хотим отказаться от подписки на конкретном канале.
HandleUpdatedData будет просто вызывать действие, переданное ему в качестве аргумента.
export function* handleUpdatedData(action) { yield put(action); }
Теперь добавим остальные sags в наш файл app/container/FeedsPage/sagas.js. Здесь мы создадим еще две функции: connectWithFeedsSocketForNewFeeds и watchConnectWithFeedsSocketForNewFeeds.
Функция connectWithFeedsSocketForNewFeeds будет отвечать за подключение к бекенд сокету и проверку новых фидов. Если есть какие-либо новые фиды, он вызовет функцию createSocketChannel из файла utils/socketSagas.js, который создаст канал событий для данного сокета. Функция connectWithFeedsSocketForNewFeeds будет содержать следующее:
function* connectWithFeedsSocketForNewFeeds() { const socket = yield call(connectToSocket); const channel = yield call(joinChannel, socket, 'feeds'); const socketChannel = yield call(createSocketChannel, channel, HAS_NEW_FEEDS, checkForNewFeeds); while (true) { const action = yield take(socketChannel); yield fork(handleUpdatedData, action); } }
И watchConnectWithFeedsSocketForNewFeeds будет иметь следующее:
export function* watchConnectWithFeedsSocketForNewFeeds() { const watcher = yield takeLatest(FETCH_FEEDS_SUCCESS, connectWithFeedsSocketForNewFeeds); // Suspend execution until location changes yield take(LOCATION_CHANGE); yield cancel(watcher); }
Теперь мы свяжем все с нашим файлом app/container/FeedsPage/index.js. Этот файл будет содержать все элементы пользовательского интерфейса. Начнем с вызова prop, который будет извлекать данные из бекенда нашего componentDidMount:
componentDidMount() { this.props.fetchFeedsRequest(); }
Это приведет к загрузке всех фидов. Теперь нам нужно снова вызвать функцию fetchFeedsRequest, когда prop hasNewFeeds истен (вы можете обратиться к начальному состоянию нашего редюсера для структуры нашего приложения):
componentWillReceiveProps(nextProps) { if (nextProps.hasNewFeeds) { this.props.fetchFeedsRequest(); } }
После этого мы просто рендерим фиды в нашей функции render. Мы создадим функцию feedsNode со следующим содержимым:
feedsNode() { return [...this.props.feeds].reverse().map((feed) => { // eslint-disable-line arrow-body-style return ( <div className="col-12" key={feed.id} > <div className="card" style={{ margin: '15px 0' }} > <div className="card-block"> <h3 className="card-title">{ feed.title }</h3> <p className="card-text">{ feed.description }</p> </div> </div> </div> ); }); }
И тогда мы можем вызвать этот метод в нашем методе render:
render() { if (this.props.loading) { return ( <div>Loading...</div> ); } return ( <div className="row"> {this.feedsNode()} </div> ); }
Если мы перейдем на http://localhost:3000/feeds, мы увидим, что на нашей консоли:
Присоединился успешно
Это означает, что наш API фидов работает отлично, и мы успешно подключили наш фронтенд к нашему бекенду. Теперь нам просто нужно создать форму, через которую мы можем ввести новый фид.
Не стесняйтесь ссылаться на мой коммит, потому что в нем выполнено многое!
Создайте форму для добавления нового фида
На этом этапе мы создадим форму, через которую мы можем добавить новый фид в нашу базу данных.
Начнем с добавления констант в app/containers/AddFeedPage/constants.js:
export const UPDATE_ATTRIBUTES = 'app/AddFeedPage/UPDATE_ATTRIBUTES'; export const SAVE_FEED_REQUEST = 'app/AddFeedPage/SAVE_FEED_REQUEST'; export const SAVE_FEED_SUCCESS = 'app/AddFeedPage/SAVE_FEED_SUCCESS'; export const SAVE_FEED_ERROR = 'app/AddFeedPage/SAVE_FEED_ERROR';
Константа UPDATE_ATTRIBUTES будет использоваться, когда мы добавим текст в поле ввода. Все остальные константы будут использоваться для сохранения заголовка и описания фида в нашей базе данных.
Контейнер AddFeedPage будет использовать четыре действия: updateAttributes, saveFeedRequest, saveFeed и saveFeedError. Функция updateAttributes обновит атрибуты нашего нового фида. Это означает, что всякий раз, когда мы вводим что-то в поле ввода заголовка и описания фида, функция updateAttributes обновляет наше состояние Redux. Эти четыре действия будут выглядеть примерно так:
export const updateAttributes = (attributes) => ({ type: UPDATE_ATTRIBUTES, attributes, }); export const saveFeedRequest = () => ({ type: SAVE_FEED_REQUEST, }); export const saveFeed = () => ({ type: SAVE_FEED_SUCCESS, }); export const saveFeedError = (error) => ({ type: SAVE_FEED_ERROR, error, });
Затем добавим функции редюсера в файл app/container/AddFeedPage/reducer.js. InitialState будет выглядеть следующим образом:
const initialState = fromJS({ feed: { data: { title: '', description: '', }, ui: { saving: false, error: null, }, }, });
И функция редюсера будет выглядеть примерно так:
function addFeedPageReducer(state = initialState, action) { switch (action.type) { case UPDATE_ATTRIBUTES: return state .setIn(['feed', 'data', 'title'], action.attributes.title) .setIn(['feed', 'data', 'description'], action.attributes.description); case SAVE_FEED_REQUEST: return state .setIn(['feed', 'ui', 'saving'], true) .setIn(['feed', 'ui', 'error'], false); case SAVE_FEED_SUCCESS: return state .setIn(['feed', 'data', 'title'], '') .setIn(['feed', 'data', 'description'], '') .setIn(['feed', 'ui', 'saving'], false); case SAVE_FEED_ERROR: return state .setIn(['feed', 'ui', 'error'], action.error) .setIn(['feed', 'ui', 'saving'], false); default: return state; } }
Затем мы будем конфигурировать наш файл app/container/AddFeedPage/selectors.js. Он будет иметь четыре селектора: title, description, error и saving. Как следует из названия, эти селекторы извлекают эти состояния из состояния Redux и делают его доступным в нашем контейнере в качестве props.
Эти четыре функции будут выглядеть следующим образом:
const title = () => createSelector( selectAddFeedPageDomain(), (titleState) => titleState.get('feed').get('data').get('title') ); const description = () => createSelector( selectAddFeedPageDomain(), (titleState) => titleState.get('feed').get('data').get('description') ); const error = () => createSelector( selectAddFeedPageDomain(), (errorState) => errorState.get('feed').get('ui').get('error') ); const saving = () => createSelector( selectAddFeedPageDomain(), (savingState) => savingState.get('feed').get('ui').get('saving') );
Затем, давайте настроим наши sagas для контейнера AddFeedPage. Он будет иметь две функции: saveFeed и watchSaveFeed. Функция saveFeed будет отвечать за выполнение запроса POST на наш API, и она будет содержать следующее:
export function* saveFeed() { const title = yield select(feedTitle()); const description = yield select(feedDescription()); const requestURL = 'http://localhost:4000/api/feeds'; try { // Call our request helper (see 'utils/Request') yield put(saveFeedDispatch()); yield call(request, requestURL, 'POST', { feed: { title, description, }, }, ); } catch (err) { yield put(saveFeedError(err)); } }
Функция watchSaveFeed будет похожа на наши предыдущие функции наблюдателей:
export function* watchSaveFeed() { const watcher = yield takeLatest(SAVE_FEED_REQUEST, saveFeed); // Suspend execution until location changes yield take(LOCATION_CHANGE); yield cancel(watcher); }
Затем нам просто нужно отобразить форму в нашем контейнере. Чтобы сохранить модульность, давайте создадим подкомпонент для формы. Создайте новый файл form.js внутри нашей папки app/container/AddFeedPage/sub-components (папка sub-components - это новая папка, которую вам нужно будет создать). Он будет содержать форму с одним полем ввода для заголовка фида и одним текстовым полем для описания фида. Метод render будет иметь следующее содержимое:
render() { return ( <form style={{ margin: '15px 0' }}> <div className="form-group"> <label htmlFor="title">Title</label> <input type="text" className="form-control" id="title" placeholder="Enter title" onChange={this.handleChange} name="title" value={this.state.title} /> </div> <div className="form-group"> <label htmlFor="description">Description</label> <textarea className="form-control" id="description" placeholder="Enter description" onChange={this.handleChange} name="description" value={this.state.description} /> </div> <button type="button" className="btn btn-primary" onClick={this.handleSubmit} disabled={this.props.saving || !this.state.title || !this.state.description } > {this.props.saving ? 'Saving...' : 'Save'} </button> </form> ); }
Мы создадим еще две функции: handleChange и handleSubmit. Функция handleChange отвечает за обновление нашего состояния Redux всякий раз, когда мы добавляем некоторый текст, а функция handleSubmit вызывает наш API для сохранения данных в нашем состоянии Redux.
Функция handleChange содержит следующее:
handleChange(e) { this.setState({ [e.target.name]: e.target.value, }); }
И функция handleSubmit будет содержать следующее:
handleSubmit() { // doing this will make the component faster // since it doesn't have to re-render on each state update this.props.onChange({ title: this.state.title, description: this.state.description, }); this.props.onSave(); this.setState({ title: '', description: '', }); }
Здесь мы сохраняем данные и очищаем значения формы.
Теперь, вернемся к нашему файлу app/container/AddFeedPage/index.js, мы просто рендерим только что созданную форму.
render() { return ( <div> <Form onChange={(val) => this.props.updateAttributes(val)} onSave={() => this.props.saveFeedRequest()} saving={this.props.saving} /> </div> ); }
Теперь весь наш кодинг завершен. Не стесняйтесь проверять мой коммит, если у вас есть какие-либо сомнения.
Завершение
Мы завершили строительство нашего приложения. Теперь мы можем посетить http://localhost:3000/feeds/new и добавить новые фиды, которые будут отображаться в реальном времени по адресу http://localhost:3000/feeds. Нам не нужно обновлять страницу, чтобы увидеть новые фиды. Вы также можете попробовать это, открыв http://localhost:3000/feeds на двух вкладках бок о бок и протестировать это!
Заключение
Это будет просто пример приложения, чтобы показать реальные возможности объединения Phoenix с React. В большинстве случаев мы используем данные в реальном времени, и это может помочь вам понять, как это происходит. Надеюсь, вы нашли этот учебник полезным.