Клон Trello на Phoenix і React. Частини 6-7



Зміст (поточний виділений матеріал)
  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

Тепер, коли back-end готовий обслуговувати запити на аутентифікацію, давайте перейдемо до front-end і подивимося, як створити та надіслати ці запити і як використовувати повернені дані для того, щоб дозволити користувачеві доступ до особистих розділів.
Файли маршрутів
Перш ніж продовжити, знову подивимося на файл маршрутів React:
// web/static/js/routes/index.js

import { IndexRoute, Route } from 'react-router';
import React from 'react';
import MainLayout from '../layouts/main';
import AuthenticatedContainer from '../containers/authenticated';
import HomeIndexView from '../views/home';
import RegistrationsNew from '../views/registrations/new';
import SessionsNew from '../views/sessions/new';
import BoardsShowView from '../views/boards/show';
import CardsShowView from '../views/cards/show';

export default (
<Route component={MainLayout}>
<Route path="/sign_up" component={RegistrationsNew} />
<Route path="/sign_in" component={SessionsNew} />

<Route path="/" component={AuthenticatedContainer}>
<IndexRoute component={HomeIndexView} />

<Route path="/boards/:id" component={BoardsShowView}>
<Route path="cards/:id" component={CardsShowView}/>
</Route>
</Route>
</Route>
);

Як ми бачили в четвертої частини,
AuthenticatedContainer
заборонить користувачам доступ до екранів дощок, крім випадків, коли jwt-токен, отриманий в результаті процесу аутентифікації, присутній і коректний.
Компонент представлення (view component)
Зараз необхідно створити компонент
SessionNew
, який буде перетворювати форму входу в додаток:
import React, {PropTypes} from 'react';
import { connect } from 'react-redux';
import { Link } from 'react-router';

import { setDocumentTitle } from '../../utils';
import Actions from '../../actions/sessions';

class SessionsNew extends React.Component {
componentDidMount() {
setDocumentTitle('Sign in');
}

_handleSubmit(e) {
e.preventDefault();

const { email password } = this.refs;
const { dispatch } = this.props;

dispatch(Actions.signIn(email.value, password.value));
}

_renderError() {
const { error } = this.props;

if (!error) return false;

return (
<div className="error">
{error}
</div>
);
}

render() {
return (
<div className='view-container sessions new'>
<main>
<header>
<div className="logo" />
</header>
<form onSubmit={::this._handleSubmit}>
{::this._renderError()}
<div className="field">
<input ref="email" type="Email" placeholder="Email" required="true" defaultValue="john@phoenix-trello.com"/>
</div>
<div className="field">
<input ref="password" type="password" placeholder="Password" required="true" defaultValue="12345678"/>
</div>
<button type="submit">Sign in</button>
</form>
<Link to="/sign_up">Create new account</Link>
</main>
</div>
);
}
}

const mapStateToProps = (state) => (
state.session
);

export default connect(mapStateToProps)(SessionsNew);

У цілому цей компонент розмальовує форму і викликає конструктор дії
signIn
при відправці останньої. Він також буде підключений до сховища, щоб мати доступ до своїх властивостях, які будуть оновлюватися за допомогою перетворювача сесії; в результаті ми зможемо показати користувачеві помилки перевірки даних.
Конструктор дії (action creator)
Слідуючи за напрямом дій користувача, створимо конструктор дії сесій:
// web/static/js/actions/sessions.js

import { routeActions } from 'redux-simple-router';
import Constants from '../constants';
import { Socket } from 'phoenix';
import { httpGet, httpPost, httpDelete } from '../utils';

function setCurrentUser(dispatch, user) {
dispatch({
type: Constants.CURRENT_USER,
currentUser: user,
});

// ...
};

const Actions = {
signIn: (email password) => {
return dispatch => {
const data = {
session: {
email: email,
password: password,
},
};

httpPost('/api/v1/sessions', data)
.then((data) => {
localStorage.setItem('phoenixAuthToken', data.jwt);
setCurrentUser(dispatch, data.user);
dispatch(routeActions.push('/'));
})
.catch((error) => {
error.response.json()
.then((errorJSON) => {
dispatch({
type: Constants.SESSIONS_ERROR,
error: errorJSON.error,
});
});
});
};
},

// ...
};

export default Actions;

Функція
signIn
створить POST-запит, передає email і пароль, вказані користувачем. Якщо аутентифікація на back-end пройшла успішно, функція збереже отриманий jwt-токен
localStorage
і направить JSON-структуру
currentUser
в сховище. Якщо з якоїсь причини результатом аутентифікації будуть помилки, замість цього функція перенаправить саме їх, а ми зможемо показати їх у формі входу в програму.
Перетворювач (reducer)
Створимо перетворювач
session
:
// web/static/js/reducers/session.js

import Constants from '../constants';

const initialState = {
currentUser: null,
error: null,
};

export default function reducer(state = initialState, action = {}) {
switch (action.type) {
case Constants.CURRENT_USER:
return { ...state, currentUser: action.currentUser, error: null };

case Constants.SESSIONS_ERROR:
return { ...state, error: action.error };

default:
return state;
}
}

Тут мало що можна додати, оскільки все очевидно з коду, тому змінимо контейнер
authenticated
, щоб він зумів обробити новий стан:
Контейнер authenticated
// web/static/js/containers/authenticated.js

import React from 'react';
import { connect } from 'react-redux';
import Actions from '../actions/sessions';
import { routeActions } from 'redux-simple-router';
import Header from '../layouts/header';

class AuthenticatedContainer extends React.Component {
componentDidMount() {
const { dispatch, currentUser } = this.props;
const phoenixAuthToken = localStorage.getItem('phoenixAuthToken');

if (phoenixAuthToken && !currentUser) {
dispatch(Actions.currentUser());
} else if (!phoenixAuthToken) {
dispatch(routeActions.push('/sign_in'));
}
}

render() {
const { currentUser, dispatch } = this.props;

if (!currentUser) return false;

return (
<div className="application-container">
<Header
currentUser={currentUser}
dispatch={dispatch}/>

<div className="main-container">
{this.props.children}
</div>
</div>
);
}
}

const mapStateToProps = (state) => ({
currentUser: state.session.currentUser,
});

export default connect(mapStateToProps)(AuthenticatedContainer);

Якщо при підключенні цього компонента токен аутентифікації вже існує, але в сховище відсутній
currentUser
, компонент викличе конструктор дії
currentUser
, щоб отримати від back-end дані користувача. Додамо його:
// web/static/js/actions/sessions.js
// ...

const Actions = {
// ...

currentUser: () => {
return dispatch => {
httpGet('/api/v1/current_user')
.then(function(data) {
setCurrentUser(dispatch, data);
})
.catch(function(error) {
console.log(error);
dispatch(routeActions.push('/sign_in'));
});
};
},

// ...
}

// ...

Це прикриє нас, коли користувач оновлює сторінку браузера або знову переходить на кореневій URL, не завершивши попередньо свій сеанс. Слідуючи за вже сказаним, після аутентифікації користувача та передачі
currentUser
в стан (state), даний компонент запустить звичайну малювання, показуючи компонент заголовка і власні вкладені дочірні маршрути.
Компонент заголовка
Даний компонент отрисует граватар ім'я користувача разом з посиланням на дошки і кнопкою виходу.
// web/static/js/layouts/header.js

import React from 'react';
import { Link } from 'react-router';
import Actions from '../actions/sessions';
import ReactGravatar from 'react-gravatar';

export default class Header extends React.Component {
constructor() {
super();
}

_renderCurrentUser() {
const { currentUser } = this.props;

if (!currentUser) {
return false;
}

const fullName = [currentUser.first_name, currentUser.last_name].join(' ');

return (
<a className="current-user">
<ReactGravatar email={currentUser.email} https /> {fullName}
</a>
);
}

_renderSignOutLink() {
if (!this.props.currentUser) {
return false;
}

return (
<a href="#" onClick={::this._handleSignOutClick}><i className="fa fa-sign-out"/> Sign out</a>
);
}

_handleSignOutClick(e) {
e.preventDefault();

this.props.dispatch(Actions.signOut());
}

render() {
return (
<header className="main-header">
<nav>
<ul>
<li>
<Link to="/"><i className="fa fa-columns"/> Boards</Link>
</li>
</ul>
</nav>
<Link to='/'>
<span className='logo'/>
</Link>
<nav className="right">
<ul>
<li>
{this._renderCurrentUser()}
</li>
<li>
{this._renderSignOutLink()}
</li>
</ul>
</nav>
</header>
);
}
}

При натисканні користувачем кнопки виходу відбувається виклик методу
singOut
конструктора дії
session
. Додамо цей метод:
// web/static/js/actions/sessions.js
// ...

const Actions = {
// ...

signOut: () => {
return dispatch => {
httpDelete('/api/v1/sessions')
.then((data) => {
localStorage.removeItem('phoenixAuthToken');

dispatch({
type: Constants.USER_SIGNED_OUT,
});

dispatch(routeActions.push('/sign_in'));
})
.catch(function(error) {
console.log(error);
});
};
},

// ...
}

// ...

Він відправить на back-end запит
DELETE
та, у разі успіху, видалить
phoenixAuthToken
localStorage
, а так само відправить дію
USER_SIGNED_OUT
, обнуляющее
currentUser
в стан (state), використовуючи раніше описаний перетворювач сесії:
// web/static/js/reducers/session.js

import Constants from '../constants';

const initialState = {
currentUser: null,
error: null,
};

export default function reducer(state = initialState, action = {}) {
switch (action.type) {
// ...

case Constants.USER_SIGNED_OUT:
return initialState;

// ...
}
}

Ще дещо
Хоча ми закінчили з процесом аутентифікації і входу в додаток, ми ще не реалізували ключову функціональність, яка стане основою всіх майбутніх можливостей, які ми запрограммируем: користувальницькі сокети і канали (the user sockets and channels). Цей момент настільки важливий, що я радше волів би залишити його для наступної частини, де ми побачимо, як виглядає
userSocket
, і як до нього підключитися, щоб у нас з'явилися двонаправлені канали між front-end і back-end, показують зміни в реальному часі.

Сокети і канали
Оригінал
У попередній частині ми завершили процес аутентифікації і тепер готові почати веселощі. З цього моменту для з'єднання front-end і back-end ми будемо багато в чому покладатися на можливості Phoenix по роботі в реальному часі. Користувачі отримають повідомлення про будь-які події, що зачіпають їх дошки, а зміни будуть автоматично відображені на екрані.
Ми можемо уявити канали (channels) в цілому як контролери. Але на відміну від обробки запиту і повернення результату в одному з'єднанні, вони обробляють двонаправлені події на задану тему, які можуть передаватися кільком підключеним користувачам. Для їх налаштування Phoenix використовує обробники сокетів (socket handlers), які аутентифицируют і ідентифікують з'єднання з сокетом, а також описують маршрути каналів, визначають, який канал опрацьовує відповідний запит.
Користувальницький сокет (user socket)
При створенні нового додатка Phoenix воно автоматично створює для нас початкову конфігурацію сокету:
# lib/phoenix_trello/endpoint.ex

defmodule PhoenixTrello.Endpoint do
use Phoenix.Endpoint, otp_app: :phoenix_trello

socket "/socket", PhoenixTrello.UserSocket

# ...
end

Створюється і
UserSocket
, але нам знадобиться внести деякі зміни в ньому, щоб обробляти потрібні повідомлення:
# web/channels/user_socket.ex

defmodule PhoenixTrello.UserSocket do
use Phoenix.Socket

alias PhoenixTrello.{Repo, User}

# Channels
channel "users:*", PhoenixTrello.UserChannel
channel "boards:*", PhoenixTrello.BoardChannel

# Transports
transport :websocket, Phoenix.Transports.WebSocket
transport :longpoll, Phoenix.Transports.LongPoll

# ...
end

По суті, у нас буде два різних канали:
  • UserChannel
    буде обробляти повідомлення на будь-яку тему, що починається з `"users:", і ми скористаємося нею, щоб інформувати користувачів про події, що відносяться до них, наприклад, якщо вони були запрошені приєднатися до дошки.
  • BoardChannel
    буде володіти основною функціональністю, обробляючи повідомлення для управління дошками, списками і картками, інформуючи будь-якого користувача, який переглядає цю дошку безпосередньо в даний момент про будь-які зміни.
Нам так само потрібно реалізувати функції
connect
та
id
, які будуть виглядати так:
# web/channels/user_socket.ex

defmodule PhoenixTrello.UserSocket do
# ...

def connect(%{"token" => token}, socket) do
case Guardian.decode_and_verify(token) do
{:ok, claims} ->
case GuardianSerializer.from_token(claims["sub"]) do
{:ok, user} ->
{:ok, assign(socket, :current_user, user)}
{:error, _reason} ->
:error
end
{:error, _reason} ->
:error
end
end

def connect(_params, _socket), do: :error

def id(socket), do: "users_socket:#{socket.assigns.current_user.id}"
end

При виклику функції
connect
(що відбувається автоматично при підключенні до сокета — прим. перекладача)
token
в якості параметра, вона перевірить токен, отримає з токена дані користувача за допомогою
GuardianSerializer
, створеного нами в частини 3, і зберігає ці дані на сокеті, так, що вони у разі необхідності будуть доступні в каналі. Більш того, вона так само заборонить підключення до сокета неаутентифицированных користувачів.
Прим. перекладачаЗверніть увагу, наведено два опису функції connect:
def connect(%{"token" => token}, socket) do ... end
та
def connect(_params, _socket), do: :error
. Завдяки механізму зіставлення з шаблоном (pattern matching) перший варіант буде викликаний при наявності в асоціативному масиві, що передається першим параметром, ключ "token" (а значення, пов'язане з цим ключем, потрапить в змінну, названу token), а другий — в будь-яких інших випадках. Функція
connect
викликається фреймворком автоматично при з'єднанні з сокетом.

Функція
id
використовується для ідентифікації поточного підключення до сокета і може використовуватися, наприклад, для завершення всіх активних каналів і сокетів для даного користувача. При бажанні це можна зробити з будь-якої частини програми, відправивши повідомлення
"disconnect"
викликом
PhoenixTrello.Endpoint.broadcast("users_socket:#{user.id}", "disconnect", %{})

до Речі, з допомогою
<AppName>.Endpoint.broadcast(topic, message, payload)
можна відправити повідомлення не тільки про відключення користувача, але і взагалі будь-яке повідомлення всім користувачам, підписаним на відповідну тему. При цьому
topic
— це рядок з темою (наприклад,
"boards:877"
),
message
— це рядок з повідомленням (наприклад,
"boards:update"
), а
payload
— асоціативний масив з даними, який перед відправкою буде перетворений в json. Наприклад, ви можете відправити користувачам, які знаходяться онлайн, якісь зміни, вироблені з допомогою REST api, прямо з контролера або з будь-якого іншого процесу.

Канал user
Після того, як ми налаштували сокет, давайте перемістимося до
UserChannel
, який дуже простий:
# web/channels/user_channel.ex

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

def join("users:" <> user_id, _params, socket) do
{:ok, socket}
end
end

Цей канал дозволить нам передавати будь-яке повідомлення, пов'язане з користувачем, звідки завгодно, обробляючи його на front-end. У нашому конкретному випадку ми скористаємося ним для передачі даних про дошці, на яку користувач був доданий в якості учасника, щоб ми могли помістити цю нову дошку у списку такого користувача. Ми також можемо використовувати канал для показу повідомлень про інших дошках, якими володіє користувач і для чого іншого, що спаде вам на думку.
Підключення до сокета і каналу
Перш ніж продовжити, згадаємо, що ми зробили в попередній частині… після аутентифікації користувача незалежно від того, використовувалася форма для входу або раніше збережений
phoenixAuthToken
, нам необхідно отримати дані
currentUser
, щоб переправити їх у сховище (store) Redux і мати можливість показати в заголовку аватар і ім'я користувача. Це виглядає непоганим місцем, щоб підключитися також до сокета і каналу, тому давайте проведемо деякий рефакторинг:
// web/static/js/actions/sessions.js

import Constants from '../constants';
import { Socket } from 'phoenix';

// ...

export function setCurrentUser(dispatch, user) {
dispatch({
type: Constants.CURRENT_USER,
currentUser: user,
});

const socket = new Socket('/socket', {
params: { token: localStorage.getItem('phoenixAuthToken') },
});

socket.connect();

const channel = socket.channel(`users:${user.id}`);

channel.join().receive('ok', () => {
dispatch({
type: Constants.SOCKET_CONNECTED,
socket: socket,
channel: channel,
});
});
};

// ...

Після переадресації даних користувача ми створюємо новий об'єкт
Socket
з JavaScript-бібліотеки
Phoenix
, передавши параметром
phoenixAuthToken
, необхідний для установки з'єднання, а потім викликаємо функцію
connect
. Ми продовжуємо створенням нового каналу користувача (user
channel
) і приєднуємося до нього. Отримавши повідомлення
ok
у відповідь на
join
, ми направляємо дію
SOCKET_CONNECTED
, щоб зберегти і сокет, і канал у сховищі:
// web/static/js/reducers/session.js

import Constants from '../constants';

const initialState = {
currentUser: null,
socket: null,
channel: null,
error: null,
};

export default function reducer(state = initialState, action = {}) {
switch (action.type) {
case Constants.CURRENT_USER:
return { ...state, currentUser: action.currentUser, error: null };

case Constants.USER_SIGNED_OUT:
return initialState;

case Constants.SOCKET_CONNECTED:
return { ...state, socket: action.socket, channel: action.channel };

case Constants.SESSIONS_ERROR:
return { ...state, error: action.error };

default:
return state;
}
}

Основна причина зберігати ці об'єкти полягає в тому, що вони знадобляться нам у багатьох місцях, так що зберігання в стан (state) робить їх доступними компонентів через властивості (
props
).
Після аутентифікації користувача, підключення до сокета і приєднання до каналу,
AuthenticatedContainer
отрисует подання
HomeIndexView
, де ми покажемо все дошки, належать користувачу, так само як і ті, куди він був запрошений в якості учасника. У наступній частині ми розкриємо, як створити нову дошку і запросити існуючих користувачів, використовуючи канали для передачі результуючих даних залученими користувачам.
А поки не забудьте поглянути на живе демо і вихідний код кінцевого результату.
Джерело: Хабрахабр

0 коментарів

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