Клон Trello на Phoenix і React. Частини 10-12. Фініш довгобуду



Зміст (поточний виділений матеріал)
  1. Введення і вибір стека технологій

  2. Початкова настройка проекту Phoenix Framework
  3. Модель User і JWT-аутентифікація
  4. Front-end для реєстрації на React і Redux
  5. Початкове заповнення бази даних і контролер для входу в додаток
  6. Автентифікація на front-end на React і Redux
  7. Налаштовуємо сокети і канали
  8. Виводимо список і створюємо нові дошки
  9. Додаємо нових користувачів дощок
  10. Відстежуємо підключених користувачів дощок
  11. Додаємо списки і картки
  12. Викладаємо проект на Heroku

Ця частина — заключна, і буде особливо довгою, але хочеться вже закінчити з циклом і піти далі. Так само прошу вибачення за настільки величезну паузу при її підготовці і публікації. Проте не пройшло даремно і дало матеріал для нових, на цей раз оригінальних статей — прим. перекладача

Відстежуємо підключення учасників дощок
Оригінал
Попередження від автора: ця частина була написана до появи функціональності Presence і є невеликим введенням в основи поведінки GenServer.
Згадаймо попередню частину, в якій ми надали нашим користувачам можливість запрошувати нових учасників на свої дошки. При додаванні e-mail існуючого користувача створювалася нова взаємозв'язок між користувачами і дошками, а дані нового користувача передавалися через канал (channel), у результаті чого його аватар відображався всім учасникам дошки, які знаходяться онлайн. На перший погляд це круто, але ми можемо зробити набагато краще і корисніше, якщо зможемо просто виділити користувачів, які в даний момент знаходяться online переглядають дошку. Давайте почнемо!
Проблема
Перш ніж продовжити, давайте задумаємося про те, чого хочемо досягти. Так, по суті, у нас є дошка та декілька учасників, які можуть несподівано відвідати її url, автоматично підключаються до каналу дошки. Коли це трапляється, зображення учасника повинен бути показаний без прозорості, на противагу учасникам, які перебувають офлайн, аватари яких повинні бути напівпрозорими.

Коли підключений учасник покидає url дошки, виходить з програми або навіть закриває вікно браузера, нам потрібно сповістити про цю подію всіх підключених до каналу дошки користувачів, щоб його аватар знову став напівпрозорим, повідомляючи, що користувач більше не переглядає дошку. Давайте розглянемо кілька способів, якими ми можемо цього досягти і їх недоліки:
  1. Управління списком підключених учасників на front-end в сховище Redux. На перший погляд це може виглядати відповідним рішенням, але воно буде працювати тільки для учасників, вже підключилися до каналу дошки. Нещодавно підключилися користувачі не матимуть цих даних.
  2. Використовувати бази даних для зберігання списку підключилися учасників. Це теж може виявитися відповідним способом, але змусить нас постійно смикати базу даних запитами списку учасників і його оновленнями при кожному підключенні або виході учасника, не кажучи вже про смешевании даних з вельми специфічною поведінкою.
Так де ми можемо зберігати цю інформацію так, щоб дати доступ до неї для всіх користувачів швидко і ефективно? Легко.… Наберіться терпіння… перманентному зберігає стан процесі.
Принципи GenServer
Хоча фраза перманентний зберігає стан процес може спочатку звучати переконливо, реалізувати це набагато простіше, ніж можна було б очікувати, завдяки Elixir його GenServer.
GenServer — це процес, подібний до будь-якого іншого процесу Elixir, і він може використовуватися для зберігання стану, асинхронного виконання коду і тому подібного.
Уявіть собі це як маленький процес, що виконується на нашому сервері і має асоціативний масив (map), що містить для кожної дошки список id підключилися користувачів. Щось на зразок такого:
%{
"1" => [1, 2, 3],
"2" => [4, 5]
}

Тепер уявіть, що цей процес має доступний інтерфейс для власної ініціалізації та оновлення асоціативного масиву стану, для додавання і видалення дощок і підключилися користувачів. Щож, це, в цілому, процес GenServer, і я кажу "в цілому" остільки, оскільки він так само буде мати відповідні переваги начебто трасировки, звіту про помилки і можливостями відстеження (supervision).
Монітор BoardChannel
Отже, створимо саму початкову версію цього процесу, який буде зберігати дані відстеження списку підключилися учасників дошки:
# /lib/phoenix_trello/board_channel/monitor.ex

defmodule PhoenixTrello.BoardChannel.Monitor do
use GenServer

#####
# Client API

def start_link(initial_state) do
GenServer.start_link(__MODULE__, initial_state, name: __MODULE__)
end
end

Працюючи з GenServer необхідно продумати як функції API для зовнішніх клієнтів, так і їх серверну реалізацію. Насамперед необхідно реалізувати функцію
start_link
, яка буде по-справжньому запускати GenServer, в якості аргументу передаючи в нього початковий стан, в нашому випадку порожній асоціативний масив, — між ім'ям модуля і назвою сервера. Ми хочемо стартувати цей процес під час запуску програми, так що додамо його до списку нащадків у нашому дереві відстеження (supervision tree):
# /lib/phoenix_trello.ex

defmodule PhoenixTrello do
use Application

def start(_type, _args) do
import Supervisor.Spec, warn: fals
e
children = [
# ...
worker(PhoenixTrello.BoardChannel.Monitor, [%{}]),
# ...
]

# ...
end
end

Тепер кожен раз при запуску програми вона автоматично викликати функцію
start_link
, яку ми тільки що створили, передаючи їй в якості початкового стану порожній асоціативний масив
%{}
. Якщо виконання
Monitor
перерветься з будь-якої причини, додаток запустить його заново з новим порожнім асоціативним масивом. Здорово, чи не правда? Тепер, коли все це, давайте почнемо додавати учасників в масив станів
Monitor
".
Обробка підключень учасників
Для цього нам знадобиться додати і клієнтську функцію, і відповідний їй серверний процесор функції зворотного зв'язку (далі просто callback-функції):
/lib/phoenix_trello/board_channel/monitor.ex
# /lib/phoenix_trello/board_channel/monitor.ex

defmodule PhoenixTrello.BoardChannel.Monitor do
use GenServer

#####
# Client API

# ...

def member_joined(board member) do
GenServer.call(__MODULE__, {:member_joined, board member})
end

#####
# Server callbacks

def handle_call({:member_joined, board member}, _from, state) do
state = case Map.get(state, board) do
nil ->
state = state
|> Map.put(board, [member])

{:reply, [member], state}
members ->
state = state
|> Map.put(board, Enum.uniq([member | members]))

{:reply, Map.get(state, board), state}
end
end
end

При виклику функції
member_joined/2
, передаючи їй дошку і користувача, що ми будемо робити звернення до процесу GenServer з повідомленням
{:member_joined, board member}
. З цієї причини нам потрібен серверний процесор callback-функції. Callback-функція
handle_call/3
з
GenServer
отримує повідомлення-запит, відправника і поточний стан. Так що в нашому випадку ми спробуємо отримати дошку з стану і додати користувача в її список користувачів. У разі якщо дошки ще немає, ми додамо її з новим списком, що містить підключився користувача. В якості відповіді ми повернемо список користувачів, що належить цій дошці.
Звідки слід викликати метод
member_joined
? З BoardChannel в момент підключення користувача:
/web/channels/board_channel.ex
# /web/channels/board_channel.ex

defmodule PhoenixTrello.BoardChannel do
use PhoenixTrello.Web, :channel

alias PhoenixTrello.{User, Board, UserBoard, List, Card, Comment, CardMember}
alias PhoenixTrello.BoardChannel.Monitor

def join("boards:" <> board_id, _params, socket) do
current_user = socket.assigns.current_user
board = get_current_board(socket, board_id)

connected_users = Monitor.user_joined(board_id, current_user.id)

send(self, {:after_join, connected_users})

{:ok, %{board: board}, assign(socket, :board, board)}
end

def handle_info({:after_join, connected_users}, socket) do
broadcast! socket, "user:joined", %{users: connected_users}

{:noreply, socket}
end

# ...
end

Таким чином, коли він підключається, ми використовуємо
Monitor
для його відстеження і розсилаємо через сокет оновлений список поточних користувачів дошки. Тепер ми можемо обробити цю розсилку на фронт-енд, щоб оновити стан додатки новим списком підключених користувачів:
// /web/static/js/actions/current_board.js

import Constants from '../constants';

const Actions = {

// ...
connectToChannel: (socket, boardId) => {
return dispatch => {
const channel = socket.channel(`boards:${boardId}`);
// ...

channel.on('user:joined', (msg) => {
dispatch({
type: Constants.CURRENT_BOARD_CONNECTED_USERS,
користувачі: msg.users,
});
});
};
}
}

Єдине, що залишилося зробити — змінити прозорість аватара в залежності від того, зазначений учасник дошки в цьому списку чи ні:
// /web/static/js/components/boards/users.js

export default class BoardUsers extends React.Component {
_renderUsers() {
return this.props.users.map((user) => {
const index = this.props.connectedUsers.findIndex((cu) => {
return cu.id === user.id;
});

const classes = classnames({ connected: index != -1 });

return (
<li className={classes} key={user.id}>
<ReactGravatar className="react-gravatar" email={user.email} https/>
</li>
);
});
}

// ...
}

Обробка відключень користувачів
Процес відключення користувача від каналу дошки майже такий же. Для початку давайте поновимо
Monitor
, додавши необхідну клієнтську функцію і відповідну їй серверну callback функцію:
/lib/phoenix_trello/board_channel/monitor.ex
# /lib/phoenix_trello/board_channel/monitor.ex

defmodule PhoenixTrello.BoardChannel.Monitor do
use GenServer

#####
# Client API

# ...

def member_left(board member) do
GenServer.call(__MODULE__, {:member_left, board member})
end

#####
# Server callbacks

# ...

def handle_call({:member_left, board member}, _from, state) do
new_members = state
|> Map.get(board)
|> List.delete(member)

state = state
|> Map.update!(board, fn(_) -> new_members end)

{:reply, new_members, state}
end
end

Як ви можете побачити, це майже та ж функціональність, що і у
member_join
, але розгорнута в зворотному порядку. В функції відбувається пошук дошки в змозі і видалення учасника, а потім заміна поточного списку учасників дошки новим і його повернення у відповіді. Так само, як і у випадку з підключенням, ми будемо викликати цю функцію BoardChannel, так що давайте його оновимо:
# /web/channels/board_channel.ex

defmodule PhoenixTrello.BoardChannel do
use PhoenixTrello.Web, :channel
# ...

def terminate(_reason, socket) do
board_id = Board.slug_id(socket.assigns.board)
user_id = socket.assigns.current_user.id

broadcast! socket, "user:left", %{users: Monitor.user_left(board_id, user_id)}

:ok
end
end

Коли підключення до каналу переривається, обробник розішле оновлений список учасників через сокет, як ми робили до цього. Для прервания підключення до каналу ми створимо конструктор дії (action creator), яким скористаємося при отмонтировании подання поточної дошки; так само нам потрібно додати обробник для розсилки
user:left
:
/web/static/js/actions/current_board.js
// /web/static/js/actions/current_board.js

import Constants from '../constants';

const Actions = {

// ...

connectToChannel: (socket, boardId) => {
return dispatch => {
const channel = socket.channel(`boards:${boardId}`);
// ...

channel.on('user:left', (msg) => {
dispatch({
type: Constants.CURRENT_BOARD_CONNECTED_USERS,
користувачі: msg.users,
});
});
};
},

leaveChannel: (channel) => {
return dispatch => {
channel.leave();
};
},
}

Не забудьте оновити компонент
BoardShowView
, щоб при його отмонтировании обробити конструктор дії
leaveChannel
:
// /web/static/js/views/boards/show.js

import Actions from '../../actions/current_board';
// ...

class BoardsShowView extends React.Component {
// ...

componentWillUnmount() {
const { dispatch, currentBoard} = this.props;

dispatch(Actions.leaveChannel(currentBoard.channel));
}

}
// ...

І на цьому все! Щоб протестувати вийшло, просто відкрийте два різних браузера і увійдіть в додаток під різними користувачами. Потім перейдіть на одну і ту ж дошку в обох і поиграйтесь, входячи і виходячи з дошки одним з користувачів. Ви побачите, як прозорість його аватара буде змінюватися туди і назад, що досить кльово.
Я сподіваюся, що ви насолодилися роботою з GenServer так само, як і я в перший раз. Але ми торкнулися тільки малу частину. GenServer і Supervisor — дуже багаті інструменти з пропонованих Elixir, причому повністю інтегровані і куленепробивні (в оригіналі автор вживає термін bullet proof, маючи на увазі, мабуть, наявну в Erlang/Elixir функціональність з відстеження циклу життя процесів та їх перезапуску в разі необхідності — прим. перекладача), не вимагають для роботи сторонніх залежностей — на противагу, наприклад, Redis. У слещующей частині ми продовжимо створення списків і карток в реальному часі за допомогою сокетів і каналів.

Додаємо списки і картки
Оригінал
У попередній частині ми створили простий, але вже корисний механізм для відстеження підключених до каналу дошки користувачів за допомогою OTP і функціональності GenServer. Ми також навчилися розсилати цей список через канал, так що кожен учасник зможе бачити, хто ще переглядає дошку в той же самий час. Тепер прийшов час дозволити учасникам додати декілька карток і списків, у той час як зміни будуть з'являтися на їх екранах негайно… Зробимо це!
Міграції та моделі
Дошка (
Board
) може мати кілька списків (lists), які, в свою чергу, також можуть мати кілька карток, так що тримаючи це в голові давайте почнемо з генерації моделі
List
, використовуючи в консолі наступну задачу
mix
:
$ mix phoenix.gen.model List lists board_id:references:board name:string
...
...
$ mix ecto.migrate

Цим ми створимо в базі даних таблицю
lists
і відповідну модель:
# web/models/list.ex

defmodule PhoenixTrello.List do
use PhoenixTrello.Web, :model

alias PhoenixTrello.{Board, List}

@derive {Poison.Encoder, only: [:id :board_id, :name]}

schema "lists" do
field :name, :string
belongs_to :board, Board

timestamps
end

@required_fields ~w(name)
@optional_fields ~w()

def changeset(model, params \\ :empty) do
model
|> cast(params, @required_fields, @optional_fields)
end
end

Генерація моделі
Card
відбувається дуже схоже:
$ mix phoenix.gen.model Card cards list_id:references:lists name:string
...
...
$ mix ecto.migrate

Результуюча модель буде виглядати якось так:
# web/models/card.ex

defmodule PhoenixTrello.Card do
use PhoenixTrello.Web, :model

alias PhoenixTrello.{Repo, List, Card}

@derive {Poison.Encoder, only: [:id :list_id, :name]}

schema "cards" do
field :name, :string
belongs_to :list List

timestamps
end

@required_fields ~w(name list_id)
@optional_fields ~w()

def changeset(model, params \\ :empty) do
model
|> cast(params, @required_fields, @optional_fields)
end
end

Не забудьте додати набір карток до схеми
lists
:
# web/models/list.ex

defmodule PhoenixTrello.List do
# ...

@derive {Poison.Encoder, only: [:id :board_id, :name, :cards]}

# ...

schema "lists" do
# ..

has_many :cards, Card
end

# ...
end

Тепер ми можемо рушити вперед, до фронтэнду і створити необхідні компоненти.
Компонент форми списку
Перш ніж продовжити, згадаємо функцію
render
компонента
BoardsShowView
:
web/static/js/views/boards/show.js
// web/static/js/views/boards/show.js

//...
//...
_renderLists() {
const { lists, channel, id, addingNewCardInListId } = this.props.currentBoard;

return lists.map((list) => {
return (
<ListCard
key={list.id}
boardId={id}
dispatch={this.props.dispatch}
channel={channel}
isAddingNewCard={addingNewCardInListId === list.id}
{...list} />
);
});
}

render() {
const { fetching, name } = this.props.currentBoard;

if (fetching) return (
<div className="view-container boards show">
<i className="fa fa-spinner fa-spin"/>
</div>
);

return (
<div className="view-container boards show">
<header className="view-header">
<h3>{name}</h3>
{::this._renderMembers()}
</header>
<div className="canvas-wrapper">
<div className="canvas">
<div className="lists-wrapper">
{::this._renderLists()}
{::this._renderAddNewList()}
</div>
</div>
</div>
{this.props.children}
</div>
);
}

На відміну від компонента
BoardMembers
, який ми створили останнім, нам також знадобиться промалювати всі списки, що відносяться до поточної дошці. На даний момент у нас немає ніяких списків, тому перейдемо до функції
_renderAddNewList
:
web/static/js/views/boards/show.js
// web/static/js/views/boards/show.js

// ...

_renderAddNewList() {
const { dispatch, formErrors, currentBoard } = this.props;

if (!currentBoard.showForm) return this._renderAddButton();

return (
<ListForm
dispatch={dispatch}
errors={formErrors}
channel={currentBoard.channel}
onCancelClick={::this._handleCancelClick} />
);
}

_renderAddButton() {
return (
<div className="list add-new" onClick={::this._handleAddNewClick}>
<div className="inner">
Add new list...
</div>
</div>
);
}

_handleAddNewClick() {
const { dispatch } = this.props;

dispatch(Actions.showForm(true));
}

_handleCancelClick() {
this.props.dispatch(Actions.showForm(false));
}

// ...

Функція
_renderAddNewList
для початку перевіряє, виставлено в
true
властивість
currentBoard.showForm
, тому що вона розмальовує кнопку Додати новий список... замість компонента
ListForm
.
Коли користувач натисне кнопку, відповідну дію (action) буде направлено в сховища і встановить властивість
showForm
на
true
, що викличе відображення форми. Тепер створимо компонент форми:
web/static/js/components/lists/form.js
// web/static/js/components/lists/form.js

import React, { PropTypes } from 'react';
import Actions from '../../actions/lists';

export default class ListForm extends React.Component {
componentDidMount() {
this.refs.name.focus();
}

_handleSubmit(e) {
e.preventDefault();

const { dispatch, channel } = this.props;
const { name } = this.refs;

const data = {
name: name.value,
};

dispatch(Actions.save(channel, data));
}

_handleCancelClick(e) {
e.preventDefault();

this.props.onCancelClick();
}

render() {
return (
<div className="list form">
<div className="inner">
<form id="new_list_form" onSubmit={::this._handleSubmit}>
<input ref="name" id="list_name" type="text" placeholder="Add a new list..." required="true"/>
<button type="submit">Save list</button> або <a href="#" onClick={::this._handleCancelClick}>cancel</a>
</form>
</div>
</div>
);
}
}


Це дуже простий компонент з формою, що містить текстое поле для назви списку, кнопкою відправки посиланням на скасування, яка буде направляти те ж дію, що ми описали, але встановлюючи
showForm
на
false
, щоб сховати форму. Коли форма відправлена компонент разом з ім'ям користувача направить конструктор дії
save
, який відправить ім'я на тему
lists:create
каналу
BoardChannel
:
// web/static/js/actions/lists.js

import Constants from '../constants';

const Actions = {
save: (channel, data) => {
return dispatch => {
channel.push('lists:create', { list: data });
};
},
};

export default Actions;

BoardChannel
Наступним кроком потрібно навчити
BoardChannel
обробляти повідомлення
lists:create
, так що займемося цим:
web/channels/board_channel.ex
# web/channels/board_channel.ex

defmodule PhoenixTrello.BoardChannel do
# ...

def handle_in("lists:create", %{"list" => list_params}, socket) do
board = socket.assigns.board

changeset = board
|> build_assoc(:lists)
|> List.changeset(list_params)

case Repo.insert(changeset) do
{:ok, list} ->
list = Repo.preload(list [:cards])

broadcast! socket, "list:created", %{list: list}

{:noreply, socket}
{:error, _changeset} ->
{:reply, {:error, %{error: "Error creating list"}}, socket}
end
end

# ...
end

Використовуючи дошку, прикріплену до каналу, функція вибудує набір змін (changeset) моделі
List
на основі отриманих параметрів (
list_params
) і додасть його в базу. Якщо все буде
:ok
, буде проведена розсилка створеного списку через канал всім підключеним користувачам, включаючи творця, тому нам не потрібно щось відповідати, і ми повертаємо просто
:noreply
. Якщо ж якимось дивом під час додавання нового списку виникне помилка, повідомлення про помилку буде повернуто тільки творцеві, так що він буде знати, що щось пішло не так.
Перетворювач
Ми майже закінчили зі списками. Канал розсилає створений лист, так що додамо обробник цього на фронтэнд в конструктор дій поточної дошки, де відбувалося підключення до каналу:
// web/static/js/actions/current_board.js

import Constants from '../constants';

const Actions = {
// ...

connectToChannel: (socket, boardId) => {
return dispatch => {
const channel = socket.channel(`boards:${boardId}`);
// ...

channel.on('list:created', (msg) => {
dispatch({
type: Constants.CURRENT_BOARD_LIST_CREATED,
list: msg.list,
});
});
};
},
// ...
}

Нарешті, нам потрібно оновити перетворювач (reducer) дошки, щоб додати список до нової версії стану, яку він повертає:
// web/static/js/reducers/current_board.js

import Constants from '../constants';

export default function reducer(state = initialState, action = {}) {

switch (action.type) {
//...

case Constants.CURRENT_BOARD_LIST_CREATED:
const lists = [...state.lists];

lists.push(action.list);

return { ...state, lists: lists, showForm: false };

// ...
}
}

Нам так само потрібно встановити аттрибут
showForm
на
false
, щоб автоматично сховати форму і знову показати кнопку Додати новий список... разом з тільки що створеним списком:


Компонент
List

Тепер на дошці є як мінімум один список, і ми можемо створити компонент
List
, яким скористаємося для малювання:
/web/static/js/components/lists/card.js
// /web/static/js/components/lists/card.js

import React, {PropTypes} from 'react';
import Actions from '../../actions/current_board';
import CardForm from '../../components/cards/form';
import Card from '../../components/cards/card';

export default class ListCard extends React.Component {
// ...

_renderForm() {
const { isAddingNewCard } = this.props;
if (!isAddingNewCard) return false;

let { id, dispatch, formErrors, channel } = this.props;

return (
<CardForm
listId={id}
dispatch={dispatch}
errors={formErrors}
channel={channel}
onCancelClick={::this._hideCardForm}
onSubmit={::this._hideCardForm}/>
);
}

_renderAddNewCard() {
const { isAddingNewCard } = this.props;
if (isAddingNewCard) return false;

return (
<a className="add-new" href="#" onClick={::this._handleAddClick}>Add a new card...</a>
);
}

_handleAddClick(e) {
e.preventDefault();

const { dispatch, id } = this.props;

dispatch(Actions.showCardForm(id));
}

_hideCardForm() {
const { dispatch } = this.props;

dispatch(Actions.showCardForm(null));
}

render() {
const { id, connectDragSource, connectDropTarget, connectCardDropTarget, isDragging } = this.props;

const styles = {
display: isDragging ? 'none' : 'block',
};

return (
<div id={`list_${id}`} className="list" style={styles}>
<div className="inner">
<header>
<h4>{this.props.name}</h4>
</header>
<div className="cards-wrapper">
{::this._renderCards()}
</div>
<footer>
{::this._renderForm()}
{::this._renderAddNewCard()}
</footer>
</div>
</div>
);
}
}

так само, як і у випадку списків, спочатку зосередимося на відображенні форми карток. В цілому ми скористаємося тим же підходом до керування або приховання форми, використовуючи властивість (
prop
), що передається основним компонентом дошки, і спрямовуючи дію для зміни властивості цього стану.


Компонент форми картки
Цей компонент буде дуже схожий на компонент
ListForm
:
/web/static/js/components/cards/form.js
// /web/static/js/components/cards/form.js

import React, { PropTypes } from 'react';
import Actions from '../../actions/lists';
import PageClick from 'react-page-click';

export default class CardForm extends React.Component {
_handleSubmit(e) {
e.preventDefault();

let { dispatch, channel } = this.props;
let { name } = this.refs;

let data = {
list_id: this.props.listId,
name: name.value,
};

dispatch(Actions.createCard(channel, data));
this.props.onSubmit();
}

componentDidMount() {
this.refs.name.focus();
}

_handleCancelClick(e) {
e.preventDefault();

this.props.onCancelClick();
}

render() {
return (
<PageClick onClick={::this._handleCancelClick}>
<div className="card form">
<form id="new_card_form" onSubmit={::this._handleSubmit}>
<textarea ref="name" id="card_name" type="text" required="true" rows={5}/>
<button type="submit">Add</button> або <a href="#" onClick={::this._handleCancelClick}>cancel</a>
</form>
</div>
</PageClick>
);
}
}

Так само, як і раніше, при відправці форми ми направляємо дію для створення картки з ім'ям, наданим користувачем. Для цього конструктор дії направить нове повідомлення в канал:
// /web/static/js/actions/lists.js

import Constants from '../constants';

const Actions = {
// ...

createCard: (channel, data) => {
return dispatch => {
channel.push('cards:create', { card data });
};
},
};

// ...

Давайте додамо обробник до
BoardChannel
:
# web/channels/board_channel.ex

def handle_in("cards:create", %{"card" => card_params}, socket) do
board = socket.assigns.board
changeset = board
|> assoc(:lists)
|> Repo.get!(card_params["list_id"])
|> build_assoc(:cards)
|> Card.changeset(card_params)

case Repo.insert(changeset) do
{:ok, card} ->
broadcast! socket, "card:created", %{card: card}

{:noreply, socket}
{:error, _changeset} ->
{:reply, {:error, %{error: "Error creating card"}}, socket}
end
end

Тим же способом, що створюючи список, новий запис
Card
буде створена ассоциированием з дошкою, прикріпленою до каналу, і зі списком, що передається як параметр. Якщо створення було успішно, запис буде переправлена всім підключеним до каналу учасникам. Нарешті, потрібно додати callback-функції до js-каналу:
// web/static/js/actions/current_board.js
//...

channel.on('card:created', (msg) => {
dispatch({
type: Constants.CURRENT_BOARD_CARD_CREATED,
card: msg.card,
});
});

// ...

І додамо нову картку до стану через перетворювач:
// web/static/js/reducers/current_board.js

// ...

case Constants.CURRENT_BOARD_CARD_CREATED:
lists = [...state.lists];
const { card } = action;

const listIndex = lists.findIndex((list) => { return list.id == card.list_id; });
lists[listIndex].cards.push(card);

return { ...state, lists: lists };

// ...

І це все! Картка буде з'являтися на екрані кожного підключеного учасника.


Що тепер?
Цим розділом ми завершили створення базової функціональності, необхідної для реєстрації користувача, входу в систему, створення дошок, запрошення на них інших людей і спільної роботи в реальному часі шляхом додавання списків і карток. Остаточна версія в репозиторії має значно більше можливостей, таких, як редагування списків, сортування списків і карток їх переміщенням, відображенням більш детальної інформації про картках, де ви так само можете призначити їм учасників і навіть додати коментарі і кольорові мітки, але ми не будемо говорити ні про одну з них, інакше це стало б вічним тренінгом. :-D
Але не хвилюйтеся, залишилася ще одна частина, де ми поговоримо про те, як поділитися результатом з усім світом, виклавши його на Heroku.

Викладаємо проект на Heroku
Оригінал
Ми нарешті зробили це (і я теж — прим. перекладача). Після 5 (в оригіналі — 11 — прим. перекладача) публікацій ми дізналися, як налаштувати новий проект Phoenix Webpack, React і Redux. Ми створили безпечну систему аутентифікації, засновану на JWT-токенах, створили міграції для необхідних нам схем нашої бази даних, запрограмували сокети і канали для функціональності реального часу і побудували процес GenServer, щоб відстежувати підключених учасників дощок. Настав час поділитися всім цим з світом, виклавши проект Heroku. Давайте зробимо це!
Налаштовуємо Heroku
Перш, ніж рухатися далі, припустимо, що у нас вже є обліковий запис Heroku встановлений Heroku Toolbelt. Щоб викласти на Heroku додаток Phoenix, нам знадобиться використовувати два різних buildpacks (набору для складання), так що створимо новий додаток, використовуючи multi-buildpack:
$ heroku create phoenix-trello --buildpack https://github.com/ddollar/heroku-buildpack-multi

Це створить новий додаток Heroku і додасть віддалений git-репозиторій
heroku
, яким ми скористаємося для публікації. Як я сказав раніше, для програми Phoenix нам буде потрібно два різних пакету для збірки:
  1. heroku-buildpack-elixir: Головний набір для складання додатків Elixir.
  2. heroku-buildpack-phoenix-staticДля компіляції статичних файлів.
Створимо файл
.buildpacks
і додамо обидва набору:
# .buildpacks

https://github.com/HashNuke/heroku-buildpack-elixir
https://github.com/gjaldon/phoenix-static-buildpack

Якщо нам потрібно змінити будь-який аспект, що відноситься до робочого оточення Elixir, ми можемо зробити це, додавши файл
elixir_buildpack.config
:
# elixir_buildpack.config

# Version Elixir
elixir_version=1.2.3

# Always rebuild from scratch on every deploy?
always_rebuild=true

У нашому випадку ми вказуємо версію Elixir, а так само вимагаємо від оточення перезбирати все, включаючи залежності, при кожній публікації. Те ж саме може бути зроблено і для статики у файлі
phoenix_static_buildpack.config
:
# phoenix_static_buildpack.config

# We can set the version of Node to use for the app here
node_version=5.3.0

# We can set the version of NPM to use for the app here
npm_version=3.5.2

В даному випадку ми вказуємо необхідні для Webpack</strong версії>
node
та
npm
. Врешті-решт ми повинні створити файл
compile
, в якому ми вкажемо, як компілювати наші ресурси після чергової публікації:

# compile

info "Building Phoenix static assets"
webpack
mix phoenix.digest

Зверніть увагу, що ми запускаємо mix-завдання
phoenix.digest
після складання
webpack
, щоб згенерувати перероблені і стислі версії ресурсів.
Налаштування робочого середовища
Перш, ніж зробити першу публікацію, необхідно оновити файл
prod.exs
, зробивши необхідні зміни конфігурації:
# config/prod.exs

use Mix.Config
# ...

config :phoenix_trello, PhoenixTrello.Endpoint,
# ..
url: [scheme: "https", host: "phoenix-trello.herokuapp.com", port: 443],
# ..
secret_key_base: System.get_env("SECRET_KEY_BASE")

# ..

# Configure your database
config :phoenix_trello, PhoenixTrello.Repo,
# ..
url: System.get_env("DATABASE_URL"),
pool_size: 20

# Configure guardian
config :guardian, Guardian,
secret_key: System.get_env("GUARDIAN_SECRET_KEY")

Головне, що ми тут робимо: змушуємо використовувати URL нашого додатка Heroku і SSL-з'єднання. Ми також використовуємо деякі змінні оточення, щоб сконфігурувати
secret_key_base
, посилання на базу даних (
url
)
secret_key
для guardian. Посилання на базу даних буде створена Heroku автоматично при першій публікації, але інші дві змінні нам потрібно згенерувати самостійно і додати їх, використовуючи командний рядок:
$ mix phoenix.gen.secret
xxxxxxxxxx
$ heroku config:set SECRET_KEY_BASE="xxxxxxxxxx"
...
...

$ mix phoenix.gen.secret
yyyyyyyyyyy
$ heroku config:set GUARDIAN_SECRET_KEY="yyyyyyyyyyy"
...
...

І ми готові до публікації!
Опублікувати
Після передачі всіх змін в наш репозиторій ми можемо опублікувати додаток просто запустивши:
$ git push heroku master
...
...
...

Якщо ми поглянемо на висновок консолі, то побачимо, як обидва набору для складання роблять свою роботу, встановлюючи Erlang і Elixir з необхідними їм залежностями, так само як і node, npm та інші речі. Нарешті, нам потрібно запустити міграцію для створення таблиць бази даних:
$ heroku run mix ecto.migrate

І на цьому все, наше додаток опубліковано і готове до роботи!
Висновок
Публікація програми Phoenix Heroku допольно проста і зрозуміла. Це може виявитися не найкращим рішенням, але для демонстраційного додатка зразок нашого працює непогано. Сподіваюся, ви насолоджувалися створенням і публікацією цього додатка настільки ж, наскільки і я. Під час написання всього циклу я зробив море змін в остаточний код, вносячи виправлення і додавши ще більше можливостей. Якщо хочете на них подивитися, не забудьте відвідати живе демо і форкнуть вихідний код кінцевого результату.
P. S. Як завжди, прошу повідомляти про опечатки будь-яким способом. Якщо у вас є краща формулювання якої-небудь фрази — не полінуйтеся мені написати, хоч в лічку, хоч в коментарі, обов'язково оновлю. Ну, і відповідь на питання про те, чи варто продовжувати такі публікації, дасть рейтинг матеріалу.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.