Pull to refresh

Строю сайты! Like a boss

Reading time 6 min
Views 3.3K
Like a boss

www.youtube.com/watch?v=NisCkxU544c

Посмотрев презентацию Юрия (yrashk) с Erlang Conference о веб-фреймворках в эрланге я заинтересовался и решил сделать сайтик не на node.js, как намеревался изначально, а используя какой-нибудь эрланговский фреймворк.

В итоге я наткнулся на фреймворк Chicago Boss. Про сам фреймворк я слышал и раньше, но пользоваться пока не довелось.



Что нам даёт Босс? В общем, они позиционируют себя как «типа рельсы, но на эрланге». С рельсами я, кстати, тоже дела совсем не имел, так что мне было всё равно. Но с MVC имел дело, т.к. на работе у нас сплошной ASP.NET MVC2. Так что базовые концепции понятны.

Имеется в наличии BossDB; это, фактически, ActiveRecord, написанный с использованием параметризированных модулей, поддерживающий разные backends. Memory, Mnesia, MongoDB, MySQL, Postgres. По заверениям разработчиков для поддержки новой базы нужно написать порядка трехсот строк кода.

Имеются Django Templates, с почти полным покрытием всех фич, фильтров и прочего.

Есть и другие штуки, с которыми я пока особо не игрался…

Инсталляция


С инсталляцией на убунте была небольшая заморочка, которая лечится скачиванием старой версии эрланга и копированием оттуда одного заголовочного файла (lib/kernel/src/inet_dns.hrl), котрый в убунтовской поставке почему-то отсутствует; ещё пришлось поставить пакет erlang-dev.

После этого делается
make
make app NewSite
cd ../NewSite


Начинаем разбираться...



В принципе, есть неплохой туториал An Evening with Chicago Boss. Но он малость устарел, и слегка не соответствует реалиям последних версий.

Задачи


  • Написать пару страничек «для всех»
  • Написать авторизацию
  • Написать редактор профиля пользователя


Структура проекта


/controller
/view
/model
/log
/static
/test

с этими тремя всё ясно
/admin - небольшая админка, можно смотреть/создавать записи для имеющихся моделей, напишу побольше дальше
/ebin - скомпилированные файлы, для запуска в рабочем режиме
/lang - файлы языковых ресурсов. Босс сам шерстит все шаблоны на предмет переводимых строк
/lib - для любого кода, который не модель и не контроллер. У меня там код проверки авторизации
/mail - у босса есть возможность вешать события на модели, и автоматом рассылать уведомления об изменениях


Если запустить проект через ./start-dev.sh — то босс автоматом будет делать горячую перезагрузку всего, так что можно разрабатывать не перезапуская сервер, что очень удобно.

Модель


Model API

Модель — параметризированный модуль эрланга, штука специфическая, я про это мало знаю. Пока просто пользуюсь как есть.

Например модель пользователя
-module(user, [Id, FirstName, LastName, Email, PasswordHash, CreateTime]).
-compile(export_all).


Вот это — уже модель. У неё есть свойства, а так-же автоматически сгенерированные методы доступа к полям. Свойства, которые заканчиваются на Id или Time — особые. Одни — для создания ссылок и зависимостей между объектами, вторые будут принимать только кортежи в формате, который отдаёт erlang:now()

После того, как код модели был сохранён в /models/user.erl — в админке появится доступ к списку текущих моделей, и документации к классу.

У модели можно создавать методы валидации, но пока мы это пропустим.

BossDB


BossDB API

Это API для работы с базой данных.
Босс хранит все записи с Id следующего вида <model_atom>-<unique_id>, где model_atom — это атом, описанный в модуле модели :), а unique_id — это числовой идентификатор, который не повторяется даже для разных моделей, так что если у вас есть модели user и article, user-1 и article-1 существовать не смогут. Учитывая, что числа в эрланге ограничены объёмом оперативной памяти — об переполнении id можно не беспокоиться :).

Особенно интересны операции поиска. У них имеется короткая форма, записывая математическими символами, типа ∈, ∉, ≁ и прочими.

Выборку можно осуществлять передавая строковой Id записи. Т.к. он префиксирован именем модели — неразберихи не будет.
User = boss_db:find("user-1").


Контроллер


Controller API

Контроллер — тоже параметризированный модуль. Но параметр у него только один. Это поле, в котором будет находиться http запрос. У запроса есть методы для доступа к полям формы (в случае POST) и заголовкам.

-module(user_controller, [Req]).
-compile(export_all).

index('GET', []) ->
    ok.


Вот простой контроллер, который просто отрендерит шаблон по умолчанию при GET запросе к адресу /user/index

Можно написать контроллер, выводящий юзера по его Id, а так-же понимающий разные методы выдачи.
Парсинг URL осуществляется следующим образом:
-module(user_controller, [Req]).
-compile(export_all).

index('GET', []) ->
    {ok, [{users, boss_db:find(user, [])}]};
index('GET', ["id", Id, "method", Method]) ->
    case boss_db:find(Id) of
        {error, Reason} ->
            boss_flash:add(Req, error, "Invalid User", "User not found"),
            ok;
        {user, User} ->
            case Method of
                "json" ->
                    {json, User} %%тут, возможно, не сработает - это просто иллюстрация
                _ ->
                    {ok, [{user, User}]}
            end
    end.          


Что-же тут происходит?
Если к нам приходит пустой запрос — то найти все объёкты модели User, и отрендерить шаблон по умолчанию, передав в него список параметров, одним из которых будет users, содержащий всех пользователей.
Если у запроса есть параметры id & method — попытаться найти пользователя по Id, в случае неудачи создать boss_flash с сообщением об ошибке и отрендерить шаблон.
Если пользователь найден то делать следующее — если метод содержит строку «json» — вернуть данные как JSON объект. Для любого другого метода — отрендреить HTML шаблон с данными пользователя.

Шаблон


Template API
используется система шаблонов Django, через библиотеку ErlyDTL

Например вот шаблон для нашей страницы /user
{{boss_flash}} <!-- в этой переменной все созданные сообщения об ошибке, которые удалятся после рендеринга -->
{% if users %}
<h1>{% trans "User List" %}</h1>
{% for user in users %}
{% trans "First Name" %} : {{ user.first_name }}</br>
{% trans "Last Name" %} : {{ user.last_name }}</br>
<a href="/user/id/{{user.id}}/method/html">{% trans "View" %}</a>
<a href="/user/id/{{user.id}}/method/json">{% trans "JSON" %}</a>
{% endfor %}
{% endif %}
{% if user %}
<h1>{% trans "User Info" %}</h1>
{% trans "First Name" %} : {{ user.first_name }}</br>
{% trans "Last Name" %} : {{ user.last_name }}
{% endif %}


Функция trans автоматом будет делать перевод строк. Так-же все строки для перевода автоматом выбираются из файлов шаблонов и попадают в веб-интерфейс перевода в админке. Файлы — обычные *.po
boss_flash это спец-конструкция для вывода временных сообщений. Не нужно париться с сессиями или передачей сообщения через параметры или скрытые поля. Очень удобно. Идентичный механизм используется в сообщениях валидаторов в ASP.Net MVC.

Авторизация


Авторизация сделана хитро. В каждом контроллере можно написать функцию before_/1, которая получает имя action и для него должна вернуть либо кортеж {ok, AdditionalParameters}, либо {redirect, "/url/to/redirect"}.
Обычно она хранится в отдельном модуле в папке /lib

-module(auth_lib).
-export(require_authentication/1).

require_authentication(Req) ->
    case boss_session:get_session_data(Req, principal) of
        {error, Reason} ->
            {redirect, "/login"};
        PrincipalId ->
            case boss_db:find(PrincipalId) of
                {error, Reason} ->
                    boss_flash:add(Req, error, "User not found", Reason),
                    {redirect, "/error"};
                {user, Principal} ->
                    {ok, Principal} % Principal - будет третьим параметром у action в контроллере
            end
    end.


Для того чтобы воспользоваться этим методом, контроллер должен иметь методы с тремя параметрами
-module(protected_controller, [Req]).
-compile(export_all).

before_("public") -> ok;
%% адрес /protected/public можно отдавать без авторизации %
before_(_) -> auth_lib:require_authentication(Req).
%% всё остальное нужно авторизировать %

index(Req, [], Principal) ->
    {ok, [{principal, Principal}]}.
    
public(Req, [])
    ok.


Если аутентикация прошла успешно, то в третий параметр будет передан текущий юзер. Иначе будет произведена переадресация на /login.
Если юзер был аутентицирован, но из базы пропала запись — будет произведена переадресация на /error, и добавлено сообщение об ошибке.

Теперь нужно написать контроллер для проверки.
-module(login_controller, [Req]).
-compile(export_all).

index('GET', []) ->
    ok;
index('POST', []) ->
    Email = Req:post_param("email"),
    PasswordHash = erlang:md5(Req:post_param("password")),
    case boss_db:find(user, [ email = Email, password_hash = PasswordHash], 1) of
        {user, Principal} ->
            boss_session:set_session_value(Req, principal, Principal.id),
            {redirect, "/protected"};
        {error, Reason} ->
            boss_flash:add(Req, error, "Invalid User"),
            ok
    end.

Примечание, знак 'равно' в выражении это не (ASCII =), а (UTF-8 =).

Заключение


Можно ещё много чего написать, но слегка устал :)
В принципе, всё пишется достаточно быстро и прикольно. Это мой первый работающий код на эрланге. Пока-что всё нравится.

Примечание: в коде статьи явно дофига ошибок, недописанных гардов и прочих мелочей. Прошу сильно не пинать, пишу на эрланге второй день.
Пишите в личку, поправлю статью.
Tags:
Hubs:
+40
Comments 63
Comments Comments 63

Articles