Эрланг для веб-разработки (1) -> Знакомство;

ab908aeb3c9c4422ab0de8abe4b4a361.png
Я начинаю публиковать серию статей о веб-разработке на Эрланге. Многие хотят попробовать Эрланг, но сталкиваются с проблемой, что вводные курсы в основном касаются Эрланга как функционального языка и далеки от реальных проектов (Learn You Some Erlang for great good! — хорошая и подробная книга). С другой стороны все обучающие материалы по веб-разработке подразумевают, что читатель уже хорошо знает Эрланг.

Эта серия статей рассчитана для разработчиков, у которых есть опыт в веб-разработке (PHP, Ruby, Java), но не имеют опыта разработки на Эрланге.

Задачей будет сделать блог. Код из статей https://github.com/denys-potapov/n2o-blog-example, готовый проект можно посмотреть по адресу http://46.101.118.21:8001/. Особенности проекта:

  • обновление комментариев в реальном времени;
  • авторизация через фейсбук;
  • данные храним в mnesia.


В основе проекта феймворк n2o. Выбор довольно субъективен, но из живых Эрланг фреймворков, n2o мне показался наиболее «эрлангоподобным», в тоже время ChicagoBoss больше похож на MVC фреймворки в других языках.

Настраиваем окружение


Я буду настраивать окружение в Ubuntu, но схожим образом должно работать и в других ОС. Скачиваем и устанавливаем актуальную версию эрланга www.erlang-solutions.com/resources/download.html.

Менеджер зависимостей


Стандартный менеджер зависимостей в Эрланге — rebar. Но, в данной статье мы будем использовать mad от создателей n2o, который совместим с rebar конфигурацией, работает быстрее и позволяет отслеживать изменения в шаблонах.

curl -fsSL https://raw.github.com/synrc/mad/master/mad > mad 
chmod +x mad 
sudo cp mad /usr/local/bin/


Для отслеживание изменений файлов mad требует установки inotify-tools:

sudo apt-get install inotify-tools


Генерируем костяк приложения и запускаем его:

    mad app "blog"
    cd blog
    mad deps compile plan repl


По адресу http://localhost:8001/ открывается чат, который обновляется по вебсокету в реальном времени, и можно переписываться самому с собой из разных окон.

3e77b70dd4814b4f8950812a48c79df0.png

Параметры mad отвечают за получение зависимостей и запуск приложения:

  • deps — получить зависимости;
  • compile — скомпилировать приложение;
  • plan — создать план запуска;
  • repl — запустить консоль.

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


Структура файлов нашего проекта стандартная для Эрланг приложений:

├── apps
    ├── rebar.config
    └── sample
        ├── ebin
        │   ├── ...
        ├── priv
        │   ├── static
        │   │   ...
        │   └── templates
        │       └── index.html
        ├── rebar.config
        └── src
            ├── index.erl
            ├── routes.erl
            ├── sample.app.src
            └── sample.erl
├── deps
├── rebar.config
└── sys.config


Подробно о структуре можно почитать в официальной документации.
Позже мы познакомимся практически со всеми файлами и папками, а пока нам надо знать, что Эрланг приложение обычно состоит из нескольких приложений, которые лежат в папке apps. У нас там одно приложение sample, в котором:

  • src — исходный код;
  • ebin — скомпилированные файлы;
  • priv — остальные файлы проекта, в данном случае шаблоны и статика;
  • index.erl — заглавная страница.

Первый код


Удалим ненужные файлы:

rm -r apps/sample/priv/static/


Для шаблонов мы используем ErlyDTL, реализацию Django Template Language на эрланге. Поэтому синтаксис будет понятен тем, кто знаком с Django-подобными шаблонизаторами (Django, Twig, Mustache).

apps/sample/priv/templates/base.html


  
    
    
    
    {% block title %}Erlang blog example{% endblock %}

    
    

    
    
    
    
  
  
      
{% block content %}{% endblock %}

apps/sample/priv/templates/index.html

{% extends "base.html" %}
{% block title %}Latest posts{% endblock %}
{% block content %}

Latest posts

{{ posts }} {% endblock %}


Теперь откроем index.erl и заменим код на такой:

-module(index).
-compile(export_all).
-include_lib("n2o/include/wf.hrl").
-include_lib("nitro/include/nitro.hrl").

main() -> #dtl{file="index"}.


В заголовке файла мы объявляем модуль, указываем, что мы экспортируем все функции из этого модуля, и подключаем два заголовочных файла.

Функция main/1 вызывается при открытии главной страницы. Функции могут возвращать или сразу HTML, или DSL Эрланг записи, о которых мы поговорим позже. Пока мы просто возвращаем отрендеренный шаблон index. В документации к Эрлангу функции всегда пишутся как название/кратность, где кратность — количество аргументов.

Знакомимся с синтаксисом


Сейчас самое время ознакомиться с основами синтаксиса, это быстрее всего сделать на www.tryerlang.org. Мы выведем на главной странице все посты. Пока не будем использовать БД, а будем хранить посты прямо в коде.

В заголовочном файле /apps/sample/include/records.hrl опишем запись для хранения постов:

-record(post, {id, title, text, author}).


Создадим модуль /apps/sample/src/posts.erl для хранения постов. Модуль экспортирует две функции: get/0 — возвращает все посты, а get/1 — возвращает пост по Id:

-module(posts).
-export([get/0, get/1]).
-include("records.hrl").

get() -> [
    #post{id=1, title="first post", text="interesting text"},
    #post{id=2, title="second post", text="not interesting text"},
    #post{id=3, title="third post", text="very interesting text"}
].

get(Id) -> lists:keyfind(Id, #post.id, ?MODULE:get()).

Записи в Эрланге — это синтаксический сахар, компилятор заменит записи на кортежи, а поля на индексы. Например #post.id будет заменен на 0.

DSL


Выше я писал, что функции могут возвращать Эрланг записи, которые преобразуются в HTML. Изменим наш index.erl, чтобы на странице выводился список всех постов:

-module(index).
-compile(export_all).
-include_lib("n2o/include/wf.hrl").
-include_lib("nitro/include/nitro.hrl").
-include_lib("records.hrl").

posts() -> [
    #panel{body=[
        #h2{body = #link{body = P#post.title, url = "/post?id=" ++ wf:to_list(P#post.id)}},
        #p{body = P#post.text}
      ]} || P <- posts:get()].

main() -> #dtl{file="index", bindings=[{posts, posts()}]}.


Для создания страницы поста, мы в /apps/sample/src/routes.erl указываем, какой модуль будет обрабатывать наш путь:

route(<<"post">>)        -> post;


Модуль apps/sample/src/post.erl просто выводит шаблон с данными поста:
модуль

-module(post).
-compile(export_all).
-include_lib("n2o/include/wf.hrl").
-include_lib("records.hrl").

main() ->
    {Id, _} = string:to_integer(binary_to_list(wf:q(<<"id">>))),
    Post = posts:get(Id),
    #dtl{file="post", bindings=[{title, Post#post.title}, {text, Post#post.text}]}.


Шаблон:

{% extends "base.html" %}
{% block title %}{{ title }}{% endblock %}
{% block content %}

{{ title }}
by {{ author }}

{{ text }}

Comments

{{ comments }} {% endblock %}

Вебсокеты


Теперь мы подошли к самому интересному, а именно связи браузера с сервером по вебсокету. Мы сделаем комментарии к посту, которые будут обновляться в реальном времени. Для этого в базовый шаблон добавим библиотеки инициализации n2o:

    
    
    
    
    
    
    
    
    
    


А в модуле post.erl добавим обработчик события и код для вывода комментариев:

main() ->
    Id = wf:to_integer(wf:q(<<"id">>)),
    Post = posts:get(Id),
    #dtl{file="post", bindings=[{title, Post#post.title}, {text, Post#post.text}, {comments, comments()}]}.

comments() ->
    [#textarea{id=comment, class=["form-control"], rows=3},
      #button{id=send, class=["btn", "btn-default"], body="Post comment",postback=comment,source=[comment]} ].

event(comment) ->
    wf:insert_bottom(comments, #blockquote{body = #p{body = wf:html_encode(wf:q(comment))}}).


При выводе кнопки, мы указываем, какое событие будет вызвано (postback) и какие параметры надо передать на сервер (source). В функции event (comment) мы отправляем клиенту код, чтобы добавить комментарий внизу списка. Пока этот комментарий не попадает к другим клиентам, но сейчас мы это исправим:

event(init) ->
    wf:reg({post, post_id()});

event(comment) ->
    wf:send({post, post_id()}, {client, wf:q(comment)});

event({client, Text}) ->
    wf:insert_bottom(comments, #blockquote{body = #p{body = wf:html_encode(Text)}}).


Событие init, вызывается в момент загрузки страницы, и мы регистрируем наш процесс, что он будет получать сообщения из пула {post, post_id ()}.

Вместо вывода комментария в событии event (comment), мы посылаем сообщение с новым комментарием в пул. А вывод комментария делаем в обработчике event ({client, Text}). Теперь мы можем весело переписываться в чате под постом, и почти повторили код, который сгенерировал mad как костяк приложения.

В следующей статье мы будем хранить посты и комментарии в БД, и добавим авторизацию через фейсбук.

© Habrahabr.ru