Создание движка для блога с помощью Phoenix и Elixir / Часть 1. Вступление
От переводчика: «Elixir и Phoenix — прекрасный пример того, куда движется современная веб-разработка. Уже сейчас эти инструменты предоставляют качественный доступ к технологиям реального времени для веб-приложений. Сайты с повышенной интерактивностью, многопользовательские браузерные игры, микросервисы — те направления, в которых данные технологии сослужат хорошую службу. Далее представлен перевод серии из 11 статей, подробно описывающих аспекты разработки на фреймворке Феникс казалось бы такой тривиальной вещи, как блоговый движок. Но не спешите кукситься, будет действительно интересно, особенно если статьи побудят вас обратить внимание на Эликсир либо стать его последователями». На данный момент наше приложение основано на:
- Elixir: v1.3.1
- Phoenix: v1.2.0
- Ecto: v2.0.2
- Comeonin: v2.5.2
Установка Phoenix
Лучшая инструкция по установке Phoenix находится на его официальном сайте.
Шаг 1. Добавление постов
Начнём с запуска mix-задачи для создания нового проекта под названием «pxblog». Для этого выполним команду `mix phoenix.new [project] [command]`. Отвечаем утвердительно на все вопросы, так как нам подойдут настройки по умолчанию.
Мы должны увидеть кучу информации о том, что создание нашего проекта было завершено, а также команды для подготовительной работы. Выполняем их поочерёдно.
Если у вас не создана база данных Postgres или приложение не настроено на работу с ней, команда `mix ecto.create` выкинет ошибку. Для её исправления откройте файл config/dev.exs и просто измените имя пользователя и пароль для роли, которая имеет права на создание базы данных:
Когда всё заработает, давайте запустим сервер и убедимся, что всё хорошо.
Для этого перейдём по адресу http://localhost:4000/ и увидим страницу Welcome to Phoenix!. Хорошая основа готова. Давайте добавим к ней главный скаффолд для работы с постами, так как всё же у нас блоговый движок.
Воспользуемся встроенным в Phoenix генератором для создания Ecto-модели, миграции и интерфейса обработки CRUD-операций модуля Post. Так как на данный момент это очень и очень простой движок, то ограничимся заголовком и сообщением. Заголовок будет строкой, а сообщение — текстом. Команда, которая создаст это всё за нас довольно простая:
Получаем ошибку! Чтобы исправить её, а заодно сделать интерфейс постов доступным из браузера, давайте откроем файл web/router.ex, и добавим в root-скоуп следующую строчку:
Теперь можем ещё раз выполнить команду и убедиться, что наша миграция прошла успешно.
И, наконец, перезапускаем наш сервер заходим на страницу http://localhost:4000/posts, где мы должны увидеть заголовок Listing posts, а также список наших постов.
Немного повозившись мы получили возможность добавлять новые посты, редактировать их и удалять. Довольно круто для такой маленькой работы!
Шаг 1Б. Написание тестов для постов
Одной из прелестей работы со скаффолдами является то, что с самого начала мы получаем набор базовых тестов. Нам даже не нужно особо их изменять до тех пор, пока мы не начнём вносить серьёзные изменения в код самого приложения. Сейчас давайте разберём какие тесты были созданы, чтобы лучше понять как писать собственные.
Во-первых, откроем файл test/models/post_test.exs и взглянем на содержимое:
Давайте разбирать этот код по частям, чтобы понять, что же тут происходит.
Наконец, мы проверяем создание ревизии с недействительными параметрами, и вместо «утверждения», что ревизия валидная, выполняем обратную операцию refute.
Это очень хороший пример того, как написать тест модели. Теперь давайте посмотрим на тест контроллера. Взглянув на него мы должны увидеть примерно следующее:
Здесь используется Pxblog.ConnCase для получения специального DSL уровня контроллера. Остальные строки должны быть уже знакомы.
Посмотрим на первый тест:
Здесь мы захватываем переменную conn, которая должна быть отправлена через блок настройки в ConnCase. Я объясню это позже. Следущий шаг — воспользоваться одноименной подходящему HTTP-методу функцией, чтобы совершить запрос по нужному пути (GET-запрос к действию :index в нашем случае). Затем мы проверяем, что ответ данного действия возвращает HTML со статусом 200 («OK») и содержит фразу Listing posts.
Следующий тест по сути такой же, только уже проверяется действие new. Всё просто.
А здесь мы делаем что-то новенькое. Во-первых, отправляем POST-запрос со списком действительных параметров по адресу post_path. Мы ожидаем получить перенаправление на список постов (действие :index). Функция redirected_to принимает в качестве аргумента объект соединения, так как нам нужно знать, куда произошло перенаправление.
Наконец, мы утверждаем, что объект, представленный этими действительными параметрами был успешно добавлен в базу данных. Эта проверка осуществляется через запрос к репозиторию Ecto Repo на поиск модели Post, соответствующей нашим параметрам @valid_attrs.
Теперь снова попробуем создать пост, но уже с недействительным списком параметров @invalid_attrs, и проверим, что снова отобразится форма создания поста.
Чтобы протестировать действие show, нам нужно создать модель Post, с которой и будем работать. Затем вызовем функцию get с хелпером post_path, и убедимся, что возвращается соответствующий ресурс.
Также мы можем попытаться запросить путь к ресурсу, которого не существует, следующим образом:
Здесь используется другой шаблон записи теста, который на самом деле довольно прост для понимания. Мы описываем ожидание того, что запрос несуществующего ресурса приведёт к ошибке 404. Туда же передаём анонимную функцию, содержащую код, который при выполнении должен вернуть эту самую ошибку. Всё просто!
Остальные тесты лишь повторяют вышеперечисленное для оставшихся путей. А вот на удалении остановимся подробнее:
В целом, всё похоже, за исключением использования HTTP-метода delete. Мы утверждаем, что должны быть перенаправлены со страницы удаления обратно к списку постов. Ещё мы используем здесь новую фишку — «отвергаем» существование объекта Post с помощью функции refute.
Шаг 2. Добавляем пользователей
Для создания модели User, мы пройдём практически те же шаги, что и при создании модели постов, за исключением добавления других столбцов. Для начала выполним:
Далее откроем файл web/router.ex и добавим следующую строчку в тот же скоуп, что и ранее:
Синтаксис здесь определяет стандартный ресурсовый путь, где первым аргументом идёт URL, а вторым — имя класса контроллера. Затем выполним:
Наконец, перезагружаем сервер и проверяем http://localhost:4000/users. Теперь, помимо постов, мы имеем возможность добавлять и пользователей!
К сожалению, пока это не очень полезный блог. В конце концов, хоть мы и можем создавать пользователей (к сожалению, сейчас это сможет сделать любой), мы не можем даже войти. Кроме того, дайджест пароля не использует никаких алгоритмов шифрования. Мы тупо храним тот текст, что ввёл пользователь! Совсем не круто!
Давайте приведём этот экран в вид более похожий на регистрацию.
Наши тесты пользователей выглядят точно так же, как и то, что было автоматически сгенерировано для наших постов, так что пока мы оставим их в покое, до тех пор, пока не начнём модифицировать логику (а этим мы займёмся прямо сейчас!).
Шаг 3. Сохранение хеша пароля, вместо самого пароля
Открыв адрес /users/new, мы видим три поля: Username, Email и PasswordDigest. Но ведь когда вы регистрируетесь на других сайтах, вас просят ввести не дайджест пароля, а сам пароль вместе с его подтверждением! Как мы можем это исправить?
В файле web/templates/user/form.html.eex удалите следующие строки:
И добавьте на их место:
После обновления страницы (должно происходить автоматически) введите данные пользователя и нажмите кнопку Submit.
Это происходит потому, что мы используем поля пароль и подтверждение пароля, о которых приложению ничего не известно. Давайте писать код, решающий эту проблему.
Начнём с изменения схемы. В файле web/models/user.ex добавим пару строк:
Обратите внимание на добавление двух полей :password и :password_confirmation. Мы объявили их в качестве виртуальных полей, так как на самом деле их не существует в нашей базе данных, но они должны существовать как свойства в структуре User. Это также позволяет применять преобразования в нашей функции changeset.
Затем мы добавим :password и :password_confirmation в список обязательных полей:
Если вы сейчас попробуете запустить тесты из файла test/models/user_test.exs, то тест “changeset with valid attributes” упадёт. Это происходит потому, что мы добавили :password и :password_confirmation к обязательным параметрам, но не обновили @valid_attrs. Давайте изменим эту строчку:
Наши тесты моделей должны снова проходить! Теперь нужно починить тесты контроллеров. Внесём некоторые изменения в файл test/controllers/user_controller_test.exs. Вначале выделим валидные атрибуты для создания объекта в отдельную переменную:
Затем изменим наш тест на создание пользователя:
И тест на обновление пользователя:
После того, как наши тесты снова стали зелёными, нам нужно добавить в changeset функцию, преобразующую пароль в дайджест:
Пока мы просто стабим поведение нашей хеширующей функции. Первым делом давайте убедимся, что изменение ревизии происходит корректно. Вернёмся в браузер на страницу http://localhost:4000/users, кликнем на ссылку New user и создадим нового пользователя с любыми данными. Теперь в списке пользователей нас ожидает новая строка, дайджест пароля в которой равен ABCDE.
Снова запустим тесты этого файла. Они проходят, но не хватает теста на проверку работы функции hash_password. Давайте добавлять:
Это большой шаг вперёд для приложения, но не такой большой для безопасности! Нужно скорее исправить хеширование пароля на настоящее с использованием BCrypt, любезно предоставленной библиотекой Comeonin.
Для этого откройте файл mix.exs и добавьте :comeonin в список applications:
Также нам нужно изменить наши зависимости. Обратите внимание на