Warning: include(/volume1/web/cyberhost.biz/wp-content/plugins/jaster_cahce/cache/top-cache.php): failed to open stream: No such file or directory in /volume1/web/cyberhost.biz/index.php on line 9
Call Stack:
0.0000 356272 1. {main}() /volume1/web/cyberhost.biz/index.php:0
Warning: include(): Failed opening '/volume1/web/cyberhost.biz/wp-content/plugins/jaster_cahce/cache/top-cache.php' for inclusion (include_path='.:/usr/share/pear') in /volume1/web/cyberhost.biz/index.php on line 9
Call Stack:
0.0000 356272 1. {main}() /volume1/web/cyberhost.biz/index.php:0
[Перевод] Клон Trello на Phoenix и React. Части 1-3 | Хостинг за 90 р. от cyberhost.biz — платный хостинг
Trello — одно из самых моих любимых приложений. Я пользуюсь им с момента появления, и мне очень нравится то, как оно работает, его простота и гибкость. Каждый раз, начиная изучать новую технологию, я предпочитаю создать полноценное приложение, в котором смогу применить на практике всё, что изучил, для решения реальных проблем, и проверить эти решения. Так что начав изучать Elixir и его Phoenix Framework я понял: я должен на практике использовать весь этот потрясающий материал, с которым познакомился, и поделиться им в виде руководства о том, как реализовать простое, но функциональное посвящение Trello.
Оглавление
Введение и выбор стека технологий
Начальная настройка проекта Phoenix Framework
Модель User и JWT-аутентификация
Front-end для регистрации на React и Redux
Начальное заполнение базы данных и контроллер для регистрации
Аутентификация на front-end на React и Redux
Настраиваем сокеты и каналы
Выводим список досок и создаём новые
Добавляем новых пользователей досок
Отслеживаем подключённых пользователей досок
Добавляем списки и карточки
Выкладываем проект на Heroku
Примечание от переводчика
В начале года, решив познакомиться с Elixir и Phoenix Framework, я наткнулся в Сети на интересный цикл статей, посвященный реализации клона Trello с помощью Elixir, Phoenix и React. Он показался мне довольно интересным, русского перевода я не нашёл, но поделиться захотелось. Наконец-то руки дошли до перевода.
Должен отметить, что с экосистемой React я совершенно незнаком, эта часть будет приведена как есть; к тому же, некоторые моменты в Elixir/Phoenix за это время изменились — проекты на месте не стоят. Так же надеюсь найти время в будущем на то, чтобы реализовать front-end с помощью Angular2 и опубликовать статью об этом, благо как раз занимаюсь связкой Angular2 <-> Phoenix Channels <-> Elixir/Phoenix Framework.
На мой взгляд, в оригинальном цикле статьи-блоки слишком короткие, поэтому одна публикация здесь будет содержать несколько частей, ссылки на оригинал будут рядом с подзаголовками.
В спорных случаях я буду давать оригинальные названия терминов, в случае расхождений переводов прошу простить и присылать альтернативные предложения. Исправления любых ошибок, опечаток и неточностей так же приветствуются.
*И прошу прощения за дублирование вступления — даже под спойлером не получилось до ката разместить и примечание, и введение от автора. Решил, что введение важнее.
Trello — одно из самых моих любимых приложений. Я пользуюсь им с момента появления, и мне очень нравится то, как оно работает, его простота и гибкость. Каждый раз, начиная изучать новую технологию, я предпочитаю создать полноценное приложение, в котором смогу применить на практике всё, что изучил, для решения реальных проблем, и проверить эти решения. Так что начав изучать Elixir и его Phoenix Framework я понял: я должен на практике использовать весь этот потрясающий материал, с которым познакомился, и поделиться им в виде руководства о том, как реализовать простое, но функциональное посвящение Trello.
Что мы собираемся сделать
По сути, мы создадим одностраничное приложение, в котором существующие пользователи смогут авторизоваться, создать несколько досок, поделиться ими с другими пользователями и добавить на них списки и карточки. Подключенные пользователи будут показаны при просмотре доски, а любые изменения автоматически немедленно — в стиле Trello — будут отражаться в браузере каждого такого пользователя.
Текущий стек технологий
Phoenix управляет статическими ресурсами с помощью npm и собирает их, прямо "из коробки" используя Branch или Webpack, так что довольно просто по-настоящему разделить front-end и back-end, при этом сохраняя единую кодовую базу. Так, для back-end мы воспользуемся:
Elixir
Phoenix Framework
Ecto
PostgreSQL
А чтобы создать одностраничное приложение для front-end:
Webpack
Sass для таблиц стилей
React
React router
Redux
ES6/ES7 JavaScript
Мы используем несколько большим количеством зависимостей Elixir’а и пакетов npm, но я расскажу о них позднее, в процессе использования.
Почему этот стек?
Elixir — очень быстрый и мощный функциональный язык, базирующийся на Erlang и имеющий дружелюбный синтаксис, весьма похожий на Ruby. Он очень надёжен и специализируется на параллельности, и благодаря виртуальной машине Erlang (Erlang VM, BEAM — прим. переводчика) может справиться с тысячами параллельных процессов. Я новичок в Elixir, так что мне всё ещё предстоит изучить немало, но исходя из уже изученного могу сказать, что это очень впечатляюще.
Мы будем использовать Phoenix — на текущий момент наиболее популярный веб-фреймворк для Elixir, который не только реализует некоторые моменты и стандарты, привнесённые в веб-разработку Rails, но и предлагает много других клёвых возможностей вроде способа управления статическими ресурсами, который я упомянул выше, и, самое важное для меня, встроенной realtime функциональности с помощью websockets без каких-либо сложностей и дополнительных внешних зависимостей (и поверьте мне — это работает как часы).
В то же время мы воспользуемся React, react-router и Redux, потому что я просто обожаю использовать это сочетание для создания одностраничных приложений и управления их состоянием. Вместо того, чтобы как обычно использовать CoffieScript, в новом году (статья была написана в начале января 2016 года — прим. переводчика) я хочу поработать с ES6 и ES7, так что это отличная возможность начать и втянуться.
Конечный результат
Приложение будет состоять из четырёх различных представлений. Первые два — экраны регистрации и входа в систему:
Главный экран будет содержать список собственных досок пользователя и досок, к которым он был подключён другими пользователями:
И, наконец, представление доски, где все пользователи смогут видеть, кто к ней подключён, а так же управлять списками и карточками:
Но довольно разговоров. Остановимся здесь, чтобы я мог начать подготовку второй части, в которой мы увидим, как создать новый проект Phoenix, что необходимо изменить, чтобы возпользоваться Webpack вместо Branch и как настроить основу для front-end.
Итак, после того, как мы выбрали текущий стек технологий, давайте начнём с создания нового проекта Phoenix. Перед этим необходимо иметь уже установленными Elixir и Phoenix, так что воспользуйтесь официальными сайтами для получения инструкций по установке.
Статические ресурсы с помощью Webpack
В отличие от Ruby on Rails Phoenix не имеет собственного конвейера обработки ресурсов (asset pipeline, некоторые русскоязычные Rails-ресурсы переводят термин как "файлопровод" — прим. переводчика), вместо этого используется Branch как средство для сборки ресурсов, что лично я считаю более современным и гибким. Прикольно, что нет необходимости использовать и Branch, если вы этого не хотите, можно воспользоваться Webpack. Я никогда не имел дела с Branch, поэтому вместо него мы применим Webpack.
Phoenix включает node.js как опциональную зависимость, поскольку она требуется для Branch, но так как Webpack тоже нуждается в node.js, удостоверьтесь, что последняя у вас установлена.
Хорошо, теперь у нас есть новый проект без средств сборки ресурсов. Создадим новый файл package.json и установим Webpack как зависимость для разработки (dev dependency — прим. переводчика):
$ npm init … (Можно просто нажать Enter в ответ на вопрос об установке значений по умолчанию) … … $ npm i webpack —save-dev
Теперь наш package.json должен выглядеть примерно так:
Для проекта нам понадобится куча зависимостей, так что вместо того, чтобы листать их все тут, пожалуйста, загляните в исходный файл в репозитории проекта и скопируйте их оттуда в свой package.json. Теперь необходимо запустить следующую команду, чтобы установить все пакеты:
$ npm install
Нам так же нужно добавить конфигурационный файл webpack.json, чтобы подсказать Webpack, как собирать ресурсы:
Здесь мы указываем, что потребуется две точки входа webpack, одна для JavaScript и вторая — для таблиц стилей, обе расположены в директории web/static. Выходные файлы будут созданы в priv/static. Так как мы собираемся воспользоваться некоторыми возможностями ES6/7 и JSX, то будем использовать Babel с некоторыми предустановками, созданными для этих целей.
Последний шаг — указать Phoenix стартовать Webpack каждый раз при запуске сервера разработки, чтобы Webpack отслеживал изменения в процессе разработки и генерировал соответствующие файлы ресурсов, на которые ссылается представление front-end’а. Для этого необходимо добавить описание ‘наблюдателя’ в файл config/dev.exs:
Ещё одна вещь, которую нужно сделать. Если мы заглянем в директорию priv/static/js, то обнаружим файл phoenix.js. Этот файл содержит всё, что нам понадобится для использования websocket и channels, так что давайте переместим его в нашу базовую директорию с исходниками web/static/js, чтобы мы могли подключить его в момент, когда это понадобится.
Основная структура front-end
Теперь у нас есть всё, чтобы начать программировать, начнём с создания структуры приложения front-end, которому, среди прочих, понадобятся следующие пакеты:
bourbon и bourbon-neat, моя самая любимая библиотека включений (mixin) для Sass
history для управления историей из JavaScript
react и react-dom
redux и react-redux для управления состоянием (state)
react-router в качестве библиотеки для маршрутизации (роутинга)
redux-simple-router для сохранения изменений маршрутов в состоянии (state)
Я не собираюсь терять время на обсуждении таблиц стилей, поскольку всё ещё правлю их, но хотел бы отметить, что для создания подходящей структуры моих Sass-файлов обычно использую css-buritto, который, по моему личному мнению, весьма полезен.
Нам нужно настроить хранилище Redux (redux store), так что создадим следующий файл:
//web/static/js/store/index.js import { createStore, applyMiddleware } from ‘redux’; import createLogger from ‘redux-logger’; import thunkMiddleware from ‘redux-thunk’; import { syncHistory } from ‘react-router-redux’; import reducers from ‘../reducers’; const loggerMiddleware = createLogger({ level: ‘info’, collapsed: true, }); export default function configureStore(browserHistory) { const reduxRouterMiddleware = syncHistory(browserHistory); const createStoreWithMiddleware = applyMiddleware(reduxRouterMiddleware, thunkMiddleware, loggerMiddleware)(createStore); return createStoreWithMiddleware(reducers); }
Фактически, мы настраиваем хранилище (Store) с тремя промежуточными слоями (middleware):
reduxRouterMiddleware для передачи действий маршрутизатора к хранилищу
redux-thunk для передачи асинхронный действий
redux-logger для логирования любых действий и изменений состояния в консоль браузера
Нам также нужно передать комбинацию преобразователей состояния (state reducers), так что создадим базовую версию этого файла:
//web/static/js/reducers/index.js import { combineReducers } from ‘redux’; import { routeReducer } from ‘redux-simple-router’; import session from ‘./session’; export default combineReducers({ routing: routeReducer, session: session, });
В качестве отправной точки нам понадобится только два преобразователя (редьюсера): routerReducer, который будет автоматически передавать изменения маршрутизации в состояние, и session, выглядящий как-то так:
Изначальное состояние последнего будет содержать объекты currentUser, который мы передадим после аутентификации посетителей, socket, которым мы воспользуемся для подключения к каналам (channels), и error для отслеживания любых проблем во время аутентификации пользователя.
Закончив с этим, мы можем перейти к нашему основному файлу application.js и отрисовать компонент Root:
//web/static/js/application.js import React from ‘react’; import ReactDOM from ‘react-dom’; import { browserHistory } from ‘react-router’; import configureStore from ‘./store’; import Root from ‘./containers/root’; const store = configureStore(browserHistory); const target = document.getElementById(‘main_container’); const node = <Root routerHistory={browserHistory} store={store}/>; ReactDOM.render(node, target);
Мы создаём объект, содержащий историю браузера, настраиваем хранилища, и, наконец, отрисовываем в основном шаблоне приложения компонент Root, который будет Redux-адаптером (wrapper) Provider для routes:
//web/static/js/containers/root.js import React from ‘react’; import { Provider } from ‘react-redux’; import { Router } from ‘react-router’; import invariant from ‘invariant’; import routes from ‘../routes’; export default class Root extends React.Component { _renderRouter() { invariant( this.props.routerHistory, ‘<Root /> needs either a routingContext or routerHistory to render.’ ); return ( <Router history={this.props.routerHistory}> {routes} </Router> ); } render() { return ( <Provider store={this.props.store}> {this._renderRouter()} </Provider> ); } }
Теперь давайте опишем очень простой файл маршрутов:
//web/static/js/routes/index.js import { IndexRoute, Route } from ‘react-router’; import React from ‘react’; import MainLayout from ‘../layouts/main’; import RegistrationsNew from ‘../views/registrations/new’; export default ( <Route component={MainLayout}> <Route path="/" component={RegistrationsNew} /> </Route> );
Наше приложение будет заключено внутрь компонента MainLayout, и корневой путь будет отрисовывать экран регистрации. Конечная версия этого файла будет несколько сложнее из-за механизма аутентификации, который мы реализуем далее, но поговорим об этом позже.
В завершении нам необходимо добавить html-контейнер, в котором мы будем отрисовывать компонент Root в основном шаблоне приложения Phoenix:
Обратите внимание, что теги link и script ссылаются на статические ресурсы, сгенерированные Webpack.
Так как мы собираемся управлять маршрутизацией на front-end, необходимо сказать Phoenix отправлять любые http-запросы на действие index контроллера PageController, который будет только отрисовывать основной шаблон и компонент Root:
# master/web/router.ex defmodule PhoenixTrello.Router do use PhoenixTrello.Web, :router pipeline :browser do plug :accepts, ["html"] plug :fetch_session plug :fetch_flash plug :protect_from_forgery plug :put_secure_browser_headers end scope "/", PhoenixTrello do pipe_through :browser # Use the default browser stack get "*path", PageController, :index end end
На данный момент это всё. В следующей публикации мы рассмотрим, как создать первую миграцию для базы данных, модель User и функциональность для создания нового пользовательского аккаунта.
Теперь, когда наш проект полностью настроен, мы готовы к созданию модели User и инструкций для миграции базы данных. В этой части мы увидим, как это сделать, а так же как позволить посетителю создать новый аккаунт пользователя.
Модель и миграция User
Phoenix использует Ecto как посредник при любом взаимодействии с базой данных. В случае с Rails можно сказать, что Ecto был бы чем-то, похожим на ActiveRecords, хотя он и делит похожую функциональность по разным модулям.
Прежде, чем продолжить, необходимо создать базу данных (но перед этим необходимо настроить параметры подключения к базе данных в config/dev.exs — прим. переводчика):
$ mix ecto.create
Теперь создадим новую миграцию и модель Ecto. Генератор модели получает в качестве параметров название модуля, его множественную форму для именования схемы и требуемые поля в виде имя:тип, так что давайте выполним:
$ mix phoenix.gen.model User users first_name:string last_name:string email:string encrypted_password:string
Если мы взглянем на получившийся файл миграции, то немедленно отметим его похожесть на файл миграции Rails:
# priv/repo/migrations/20151224075404_create_user.exs defmodule PhoenixTrello.Repo.Migrations.CreateUser do use Ecto.Migration def change do create table(:users) do add :first_name, :string, null: false add :last_name, :string, null: false add :email, :string, null: false add :crypted_password, :string, null: false timestamps end create unique_index(:users, [:email]) end end
Я добавил запрет на содержание null в содержимом полей и даже уникальный индекс для поля email. Делаю это потому, что предпочитаю переложить ответственность за целостность данных на базу данных вместо того, чтобы полагаться на приложение, как делают многие другие разработчики. Думаю, это просто вопрос персональных предпочтений.
Теперь давайте создадим в базе данных таблицу users:
$ mix ecto.migrate
Настало время посмотреть на модель User поближе:
# web/models/user.ex defmodule PhoenixTrello.User do use Ecto.Schema import Ecto.Changeset schema "users" do field :first_name, :string field :last_name, :string field :email, :string field :encrypted_password, :string timestamps end @required_fields ~w(first_name last_name email) @optional_fields ~w(encrypted_password) def changeset(model, params \ :empty) do model |> cast(params, @required_fields, @optional_fields) end end
В ней можно увидеть два основных раздела:
Блок схемы (schema), в котором расположены все метаданные, относящиеся к полям таблицы
Функция changeset, в которое можно определить все проверки и трансформации, применяемые к данным до того, как они будут готовы к использованию в нашем приложении.
Прим. переводчика: В последние версии Ecto были внесены некоторые изменения. Например, атом :empty помечен как нерекомендуемый (deprecated), вместо него необходимо использовать пустой ассоциативный массив (map) %{}, а функцию cast/4 рекомендуется заменить на связку cast/3 и validate_required/3. Естественно, генератор последних версий Phoenix этим рекомендациям следует.
Проверки и трансформации набора изменений (changeset)
Итак, когда пользователь регистрируется, мы хотели бы дополнительно ввести некоторые проверки, поскольку ранее добавили запрет на использование null в качестве значения полей и ввели требование уникальности email. Мы обязаны отразить это в модели User, чтобы обработать возможные ошибки, вызванные некорректными данными. Так же хотелось бы зашифровать поле encrypted_field так, чтобы даже несмотря на использование простой строки в качестве пароля записан он был в защищённом виде.
Давайте обновим модель и для начала добавим некоторые проверки:
# web/models/user.ex defmodule PhoenixTrello.User do # … schema "users" do # … field :password, :string, virtual: true # … end @required_fields ~w(first_name last_name email password) @optional_fields ~w(encrypted_password) def changeset(model, params \ :empty) do model |> cast(params, @required_fields, @optional_fields) |> validate_format(:email, ~r/@/) |> validate_length(:password, min: 5) |> validate_confirmation(:password, message: "Password does not match") |> unique_constraint(:email, message: "Email already taken") end end
В основном, мы сделали следующие модификации:
добавили новое виртуальное поле password, которое не будет записано в базу данных, но может использоваться как любое другое поле для любых иных целей. В нашем случае мы будем его заполнять из формы регистрации
сделали поле password обязательным
добавили проверку формата поля email
добавили проверку пароля, требуя его длины минимум в 5 символов; также будет проверяться массив параметров на предмет идентичности пароля с полем password_confirmation
добавили ограничение уникальности для проверки на наличие уже существующего email
Этими изменениями мы покрыли все требуемые проверки. Однако до записи данных также необходимо заполнить поле encrypted_password. Для этого воспользуемся библиотекой хэширования паролей comeonin, добавив её в mix.exs как приложение и зависимость:
# mix.exs defmodule PhoenixTrello.Mixfile do use Mix.Project # … def application do [mod: {PhoenixTrello, []}, applications: [ # … :comeonin ] ] end #… defp deps do [ # … {:comeonin, "~> 2.0"}, # … ] end end
Не забудьте установить библиотеку командой:
$ mix deps.get
После установки comeonin давайте вернёмся к модели User и для генерации encrypted_password добавим новый шаг к цепочке changeset:
# web/models/user.ex defmodule PhoenixTrello.User do # … def changeset(model, params \ :empty) do model # … другие проверки и ограничения |> generate_encrypted_password end defp generate_encrypted_password(current_changeset) do case current_changeset do %Ecto.Changeset{valid?: true, changes: %{password: password}} -> put_change(current_changeset, :encrypted_password, Comeonin.Bcrypt.hashpwsalt(password)) _ -> current_changeset end end end
В этом новом методе мы сначала проверяем, корректны ли изменения в наборе и изменился ли пароль. Если да, мы шифруем пароль с помощью comeonin и помещаем результат в поле encrypted_password нашего набора, в противном случае возвращаем набор как есть.
Маршрутизатор
Теперь, когда модель User готова, продолжим реализацию процесса регистрации, добавив в файл router.ex цепочку :api и наш первый маршрут:
# web/router.ex defmodule PhoenixTrello.Router do use PhoenixTrello.Web, :router #… pipeline :api do plug :accepts, ["json"] end scope "/api", PhoenixTrello do pipe_through :api scope "/v1" do post "/registrations", RegistrationController, :create end end #… end
Так, любой запрос POST к /api/v1/registrations будет обработан действием :create контроллера RegistrationController, принимающего данные в формате json… в целом, всё довольно очевидно 🙂
Контроллер
До начала реализации контроллера давайте подумаем, что же нам нужно. Посетитель зайдёт на страницу регистрации, заполнит форму и отправит её. Если данные, полученные контроллером, корректны, нам потребуется добавить нового пользователя в базу данных, ввести его в систему и вернуть front-end’у в формате json данные вместе токеном аутентификации jwt в качестве результата входа в систему. Этот токен — то, что потребуется не только для отправки с каждым запросом для аутентификации пользователя, но и для доступа пользователя к защищённым экранам приложения.
Чтобы реализовать аутентификацию и генерацию jwt, воспользуемся библиотекой Guardian, которая очень неплохо справляется с этой задачей. Просто добавьте следующее в mix.exs:
# mix.exs defmodule PhoenixTrello.Mixfile do use Mix.Project #… defp deps do [ # … {:guardian, "~> 0.9.0"}, # … ] end end
После запуска mix deps.get потребуется внести настройки библиотеки в config.exs:
Теперь готово всё, чтобы реализовать RegistrationController:
# web/controllers/api/v1/registration_controller.ex defmodule PhoenixTrello.RegistrationController do use PhoenixTrello.Web, :controller alias PhoenixTrello.{Repo, User} plug :scrub_params, "user" when action in [:create] def create(conn, %{"user" => user_params}) do changeset = User.changeset(%User{}, user_params) case Repo.insert(changeset) do {:ok, user} -> {:ok, jwt, _full_claims} = Guardian.encode_and_sign(user, :token) conn |> put_status(:created) |> render(PhoenixTrello.SessionView, "show.json", jwt: jwt, user: user) {:error, changeset} -> conn |> put_status(:unprocessable_entity) |> render(PhoenixTrello.RegistrationView, "error.json", changeset: changeset) end end end
Благодаря механизму сопоставления с шаблоном (pattern matching), действие create ожидает в параметрах ключ "user". С этими параметрами мы создадим набор User и добавим его в базу данных. Если всё будет хорошо, мы воспользуемся Guardian для кодирования и подписи (метод encode_and_sign) данных нового пользователя, получив токен jwt и преобразуя его вместе с данными о пользователе в json. В противном случае, если набор данных некорректен, мы отобразим ошибки в виде json так, что сможем показать их пользователю в форме регистрации.
Сериализация JSON
Phoenix в качестве библиотеки JSON по-умолчанию использует Poison. Так как это одна из зависимостей Phoenix, для её установки нам не потребуется делать что-то особенное. Что же действительно нужно сделать — так это обновить модель User и указать, какие поля необходимо сериализовать:
# web/models/user.ex defmodule PhoenixTrello.User do use PhoenixTrello.Web, :model # … @derive {Poison.Encoder, only: [:id, :first_name, :last_name, :email]} # … end
С этого момента когда мы будем конвертировать данные о пользователе или список пользователей в ответ на действие в контроллере или канале (channel), библиотека просто вернёт указанные поля. Проще паренной репы!
Получив back-end, готовый к регистрации новых пользователей, в следующей публикации мы переместимся к front-end, и чтобы завершить процесс регистрации, запрограммируем несколько прикольных штук на React и Redux. А тем временем не забудьте взглянуть на живое демо и исходный код конечного результата.
Warning: include(/volume1/web/cyberhost.biz/wp-content/plugins/jaster_cahce/cache/bottom-cache.php): failed to open stream: No such file or directory in /volume1/web/cyberhost.biz/index.php on line 13 Call Stack: 0.0000 356272 1. {main}() /volume1/web/cyberhost.biz/index.php:0 Warning: include(): Failed opening '/volume1/web/cyberhost.biz/wp-content/plugins/jaster_cahce/cache/bottom-cache.php' for inclusion (include_path='.:/usr/share/pear') in /volume1/web/cyberhost.biz/index.php on line 13 Call Stack: 0.0000 356272 1. {main}() /volume1/web/cyberhost.biz/index.php:0
Свежие комментарии