Статьи

Как создать ленту в реальном времени с использованием Phoenix и React

Конечный продукт
Что вы будете создавать

В этом уроке я покажу вам, как мы можем использовать возможности React и Phoenix для создания приложения каналов, которое будет обновляться в реальном времени по мере добавления новых каналов в нашу базу данных.

Elixir известен своей стабильностью и функциями реального времени, а Phoenix использует способность Erlang VM обрабатывать миллионы соединений наряду с красивым синтаксисом Elixir и производительными инструментами. Это поможет нам генерировать в реальном времени обновление данных через API, которые будут использоваться нашим приложением React для отображения данных в пользовательском интерфейсе.

У вас должны быть установлены Elixir, Erlang и Phoenix. Подробнее об этом можно узнать на веб-сайте платформы Phoenix . Кроме того, мы будем использовать базовый шаблон React, поскольку он в хорошем состоянии и надлежащим образом задокументирован.

В этом разделе мы загрузим наше приложение только для API Phoenix и добавим каналы для обновления 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

Нам придется удалить поля имени пользователя и пароля из нашего файла config / dev.exs, так как мы будем создавать нашу базу данных без имени пользователя или пароля. Это просто для простоты этого поста. Для вашего приложения убедитесь, что вы сначала создали базу данных с именем пользователя и паролем.

mix ecto.create

Приведенная выше команда создаст нашу базу данных. Теперь мы можем запустить наш сервер Phoenix и проверить, все ли в порядке на этом этапе.

mix phoenix.server

Приведенная выше команда запустит наш сервер Phoenix, и мы можем зайти на http: // localhost: 4000, чтобы увидеть, как он работает. В настоящее время будет выдано сообщение об отсутствии маршрута, поскольку мы еще не создали ни одного маршрута!

Не стесняйтесь проверять ваши изменения с моим коммитом .

На этом этапе мы добавим нашу модель Feed в наше приложение Phoenix. Модель Feeds будет состоять из заголовка и описания .

mix phoenix.gen.json Feed feeds title:string description:string

Приведенная выше команда сгенерирует нашу модель подачи и контроллер. Это также сгенерирует спецификации (которые мы не будем изменять в этом уроке, просто для краткости).

Вам необходимо добавить маршрут /feeds в ваш файл web / router.ex внутри области API :

resources "/feeds", FeedController, except: [:new, :edit]

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

mix ecto.migrate

Теперь, если мы перейдем по адресу http: // localhost: 4000 / api / feeds , мы увидим, что API отправляет нам пустой ответ, поскольку в нашей таблице каналов нет данных.

Вы можете проверить мой коммит для справки.

На этом этапе мы добавим наш канал Feed в наше приложение Phoenix. Каналы предоставляют средства для двунаправленной связи от клиентов, которые интегрируются со слоем Phoenix.PubSub для мягкой функциональности в реальном времени.

mix phoenix.gen.channel feed

Приведенная выше команда создаст файл feed_channel.ex в папке web / channel . Через этот файл наше приложение React будет обмениваться обновленными данными из базы данных с использованием сокетов.

Нам нужно добавить новый канал в наш файл web / channel / user_socket.ex :

channel "feeds", RealtimeFeedApi.FeedChannel

Поскольку мы не делаем никакой аутентификации для этого приложения, мы можем изменить наш файл web / channel / feed_channel.ex . Нам понадобится один метод join для нашего приложения React, чтобы присоединиться к нашему каналу фида, один Метод handle_out для передачи полезной нагрузки через сокетное соединение и один метод broadcast_create, который будет транслировать полезную нагрузку при создании нового фида в базе данных.

1
2
3
def join(«feeds», payload, socket) do
  {:ok, «Joined feeds», socket}
end
1
2
3
4
def handle_out(event, payload, socket) do
  push socket, event, payload
  {:noreply, socket}
end
1
2
3
4
5
6
7
8
9
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 . Наш метод создания будет выглядеть примерно так:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
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

Создание Метод отвечает за вставку новых данных в базу данных. Вы можете проверить мой коммит для справки.

Нам необходимо реализовать эту поддержку, поскольку в нашем случае API обслуживается с http: // localhost: 4000, но наше интерфейсное приложение будет работать на http: // localhost: 3000 . Добавить поддержку CORS легко. Нам просто нужно добавить cors_plug в наш файл mix.exs :

1
2
3
4
5
6
defp deps do
  [
   …
   {:cors_plug, «~> 1.3»}
  ]
end

Теперь мы остановим наш сервер Phoenix, используя Control-C, и получим зависимость, используя следующую команду:

mix deps.get

Нам потребуется добавить следующую строку в наш файл lib / realtime_feed_api / endpoint.ex :

plug CORSPlug

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

Как уже упоминалось ранее, мы будем использовать реактор-образец, чтобы начать работу с нашим внешним приложением. Мы будем использовать 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

Это инициализирует новый проект с этим шаблоном, удаляет историю react-boilerplate реактивного react-boilerplate , устанавливает зависимости и инициализирует новый репозиторий.

Теперь давайте удалим пример приложения, представленного в шаблоне, и заменим его на наименьшее количество шаблонного кода, необходимого для начала написания нашего приложения:

npm run clean

Теперь мы можем запустить наше приложение с помощью npm run start и увидеть, что оно работает по адресу http: // localhost: 3000 / .

Вы можете сослаться на мой коммит .

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

npm run generate container

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

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

Давайте просто быстро добавим наши контейнеры в наш файл rout.js :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
{
  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 :

1
2
3
4
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 :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
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 / Containers / FeedsPage / reducer.js :

01
02
03
04
05
06
07
08
09
10
11
12
const initialState = fromJS({
  feeds: {
    data: List(),
    ui: {
      loading: false,
      error: false,
    },
  },
  metadata: {
    hasNewFeeds: false,
  },
});

Это будет initialState нашего приложения (состояние до начала выборки данных). Поскольку мы используем ImmutableJS , мы можем использовать его структуру данных List для хранения наших неизменных данных. Наша функция редуктора будет выглядеть примерно так:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
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. Мы можем извлечь значения сложных состояний очень легко с помощью повторного выбора. Давайте добавим следующие селекторы в наш файл app / container / FeedsPage / selectors.js :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
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 для извлечения данных из нашего состояния. Вам просто нужно запомнить синтаксис повторного выбора .

Пришло время добавить наши саги, используя Redx-сагу . Здесь основная идея заключается в том, что нам нужно создать функцию для извлечения данных и другую функцию для наблюдения за начальной функцией, чтобы при каждом отправлении какого-либо конкретного действия нам нужно было вызывать начальную функцию. Давайте добавим функцию, которая будет извлекать наш список каналов из внутреннего приложения в нашем файле app / container / FeedsPage / sagas.js :

01
02
03
04
05
06
07
08
09
10
11
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 для нашего бэкенда. Весь файл доступен на реакторе . После внесения изменений в файл sagas.js мы внесем в него небольшие изменения.

Нам также нужно создать еще одну функцию для наблюдения за функцией getFeeds :

1
2
3
4
5
6
7
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 из реактора-кипариса в наше приложение в папке app / utils, а затем изменим функцию запроса :

01
02
03
04
05
06
07
08
09
10
11
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);
}

Я только что добавил несколько значений по умолчанию, которые помогут нам в дальнейшем сократить код, поскольку нам не нужно каждый раз передавать метод и заголовки. Теперь нам нужно создать еще один файл утилит в папке app / utils . Мы назовем этот файл socketSagas.js . Он будет содержать четыре функции: connectToSocket , joinChannel , createSocketChannel и handleUpdatedData .

Функция connectToSocket будет отвечать за подключение к нашему внутреннему API-сокету. Мы будем использовать пакет phoenix npm. Поэтому нам нужно будет установить его:

npm install phoenix --save

Это установит пакет phoenix npm и сохранит его в нашем файле package.json . Наша функция connectToSocket будет выглядеть примерно так:

1
2
3
4
5
export function* connectToSocket() {
  const socket = new Socket(‘ws:localhost:4000/socket’);
  socket.connect();
  return socket;
}

Далее мы определяем наши Функция joinChannel , которая будет отвечать за присоединение к определенному каналу с нашего бэкенда . Функция joinChannel будет иметь следующее содержимое:

01
02
03
04
05
06
07
08
09
10
11
12
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;
}

Если присоединение прошло успешно, мы зарегистрируем сообщение «Присоединено успешно» только для тестирования. Если на этапе присоединения произошла ошибка, мы также запишем ее только для целей отладки.

createSocketChannel будет отвечать за создание канала событий из заданного сокета.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
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 просто вызовет действие, переданное ему в качестве аргумента.

1
2
3
export function* handleUpdatedData(action) {
  yield put(action);
}

Теперь давайте добавим остальные саги в наш файл app / container / FeedsPage / sagas.js . Мы создадим еще две функции здесь: connectWithFeedsSocketForNewFeeds и watchConnectWithFeedsSocketForNewFeeds .

Функция connectWithFeedsSocketForNewFeeds будет отвечать за соединение с внутренним сокетом и проверку новых каналов. Если есть какие-либо новые каналы, он будет вызывать Функция createSocketChannel из файла utils / socketSagas.js , которая создает канал событий для данного сокета. Наша функция connectWithFeedsSocketForNewFeeds будет содержать следующее:

01
02
03
04
05
06
07
08
09
10
11
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 будет иметь следующее:

1
2
3
4
5
6
7
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 . Этот файл будет содержать все наши элементы пользовательского интерфейса. Давайте начнем с вызова опоры, которая будет извлекать данные из серверной части в нашем componentDidMount:

1
2
3
componentDidMount() {
  this.props.fetchFeedsRequest();
}

Это принесет все каналы. Теперь нам нужно снова вызывать реквизит fetchFeedsRequest всякий раз, когда реквизит hasNewFeeds истинен (вы можете обратиться к initialState нашего редуктора для структуры нашего приложения):

1
2
3
4
5
componentWillReceiveProps(nextProps) {
   if (nextProps.hasNewFeeds) {
     this.props.fetchFeedsRequest();
   }
 }

После этого мы просто визуализируем каналы в нашей функции рендеринга. Мы создадим функцию feedsNode со следующим содержанием:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
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>
    );
  });
}

И затем мы можем вызвать этот метод в нашем методе рендеринга :

01
02
03
04
05
06
07
08
09
10
11
12
13
render() {
  if (this.props.loading) {
    return (
      <div>Loading…</div>
    );
  }
 
  return (
    <div className=»row»>
      {this.feedsNode()}
    </div>
  );
}

Если мы теперь перейдем по адресу http: // localhost: 3000 / feeds , мы увидим следующее, зарегистрированное в нашей консоли:

Joined successfully Joined feeds

Это означает, что наш API фидов работает нормально, и мы успешно соединили наш интерфейс с нашим фоновым приложением. Теперь нам просто нужно создать форму, с помощью которой мы сможем ввести новый канал.

Не стесняйтесь ссылаться на мой коммит, так как в этом коммите было много материала

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

Давайте начнем с добавления констант в наш файл app / container / AddFeedPage / constants.js :

1
2
3
4
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. Эти четыре действия будут выглядеть примерно так:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
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 будет выглядеть следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
const initialState = fromJS({
  feed: {
    data: {
      title: »,
      description: »,
    },
    ui: {
      saving: false,
      error: null,
    },
  },
});

И функция редуктора будет выглядеть примерно так:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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 . У него будет четыре селектора: название , описание , ошибка и экономия Как следует из названия, эти селекторы извлекут эти состояния из состояния Redux и сделают его доступным в нашем контейнере в качестве реквизита.

Эти четыре функции будут выглядеть следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
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’)
);

Далее, давайте настроим наши саги для контейнера AddFeedPage . Он будет иметь две функции: saveFeed и watchSaveFeed . Функция saveFeed будет отвечать за выполнение запроса POST к нашему API и будет иметь следующее:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
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 будет похожа на наши предыдущие функции часов:

1
2
3
4
5
6
7
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 / Containers / AddFeedPage / sub-component (папка sub-component — это новая папка, которую вам нужно будет создать). Он будет содержать форму с одним полем ввода для заголовка канала и одной текстовой областью для описания канала. Метод render будет иметь следующее содержимое:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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.props.saving ?
      </button>
    </form>
  );
}

Мы создадим еще две функции: handleChange и handleSubmit . The HandChange Функция отвечает за обновление нашего состояния Redux всякий раз, когда мы добавляем некоторый текст, а функция handleSubmit вызывает наш API для сохранения данных в нашем состоянии Redux.

Функция handleChange имеет следующее:

1
2
3
4
5
handleChange(e) {
  this.setState({
    [e.target.name]: e.target.value,
  });
}

И ручка отправить Функция будет содержать следующее:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
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 , мы просто отрендерим только что созданную форму.

01
02
03
04
05
06
07
08
09
10
11
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. В настоящее время мы используем данные в реальном времени в большинстве мест, и это может помочь вам почувствовать, как разрабатывать что-то подобное. Я надеюсь, что вы нашли этот урок полезным.