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