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