Pull to refresh

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

Reading time 7 min
Views 29K

Продолжение о базе данных и деплое во второй статье.

Я начинаю публиковать серию статей о веб-разработке на Эрланге. Многие хотят попробовать Эрланг, но сталкиваются с проблемой, что вводные курсы в основном касаются Эрланга как функционального языка и далеки от реальных проектов (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/ открывается чат, который обновляется по вебсокету в реальном времени, и можно переписываться самому с собой из разных окон.



Параметры 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
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>{% block title %}Erlang blog example{% endblock %}</title>

    <!-- Bootstrap -->
    <link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css" rel="stylesheet">

    <!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->
    <!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
    <!--[if lt IE 9]>
      <script src="//oss.maxcdn.com/html5shiv/3.7.2/html5shiv.min.js"></script>
      <script src="//oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
    <![endif]-->
    <style>
      .container {
          max-width: 40em;
      }
    </style>
  </head>
  <body>
      <div class="container">
        {% block content %}{% endblock %}
      </div>

    <script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js"></script>
  </body>
</html>



apps/sample/priv/templates/index.html
{% extends "base.html" %}
{% block title %}Latest posts{% endblock %}
{% block content %}
<h1>Latest posts</h1>
{{ 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 %}
<h1>{{ title }}<br />
<small>by {{ author }}</small>
<p>{{ text }}</p>
<h3>Comments</h3>
{{ comments }}
{% endblock %}


Вебсокеты


Теперь мы подошли к самому интересному, а именно связи браузера с сервером по вебсокету. Мы сделаем комментарии к посту, которые будут обновляться в реальном времени. Для этого в базовый шаблон добавим библиотеки инициализации n2o:
    <script>{{script}}</script>
    <script src='/n2o/protocols/bert.js'></script>
    <script src='/n2o/protocols/client.js'></script>
    <script src='/n2o/protocols/nitrogen.js'></script>
    <script src='/n2o/validation.js'></script>
    <script src='/n2o/bullet.js'></script>
    <script src='/n2o/utf8.js'></script>
    <script src='/n2o/template.js'></script>
    <script src='/n2o/n2o.js'></script>
    <script>protos = [ $bert, $client ]; N2O_start();</script>

А в модуле 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 как костяк приложения.

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

Articles