среда, 24 ноября 2010 г.

Пример приложения на Mojolicious ( не Lite )

Пример приложения на Mojolicious ( не Lite )
В интернете много всего уже сказано про Mojolicious::Lite, но сегодня про Mojolicious (не Lite). С фреймворком я только начал знакомится, поэтому смело критикуйте в комментариях. Для изучения фреймворака я решил написать прототип сервиса заметок. Сразу прошу не писать про XSRF,  я в курсе и это лишь прототип. Расписывать все я не буду, только ключевые моменты. Основную информацию Вы сможете почерпнуть непосредственно из кода.
Почему Mojolicious?
Mojolicious - очередной MVC веб-фрейворк. "Очередной велосипед, хоть и с более круглыми колесами", как кто-то он нем отозвался на одном из форумов.



На CPAN достаточно много веб-фреймворков, но мой выбор пал на это по следующим причинам:
  1. Отсутствие внешних зависимостей, кроме Perl 5.8.7 (значит работает везде, где есть Perl). Попробуйте установить с CPAN "Catalyst" и "Mojolicious" - почувствуйте разницу. Но притом, фреймворк содержит все необходимое (кроме модели ;) ), включая шаблонизатор.
  2. Легковесность фрейворка. Mojolicious хоть и MVC, но включает только View и Controller. Model - на Ваше усмотрение, никто Вас не принуждает ни к чему. Вообще Mojolicious - это компактное ядро, а для расширения функционала используются плагины.
  3. Концептуальная целостность. Разрабатывает фреймворк 1 человек - Sebastian Riedel, поэтому API полностью согласован. Нет никакой магии, чистый перловый OOP.
  4. Удобство. Чтобы полностью развернуть приложение на машине разработчика Вам потребуется минимум времени. Установили Mojolicious и ./myapp daemon. Если Вы разрабатываете OpenSource проект и хотите, чтобы он был популярный, то Mojolicious, как минимум, облегчит развертывание.
  5. Свежесть. Фреймворк свежий, не закис еще. Динамично развивается. Поддерживает PSGI, RESTful роуты, Signed Cookies.
  6. Учтен опыт разработки Catalyst (Sebastian Riedel один из разработчиков Catalyst).
Конечно это все IMHO, кто-то не хочет велосипедов и хочет исполозовать модули с CPAN с которыми он привык работать. Хочет, чтобы Model входила в фреймворк и хочет, чтобы будущее фреймворка не завилило от одного человека.

Документация по Mojolicious:
  • perldoc Mojolicious
  • perldoc Mojolicious::Contoller
  • perldoc Mojolicious::Guides
  • Mojolicious wiki
  • Routes for non lite apps
  • Исходники самого Mojolicious (только берите последнюю версию с Gihub). Себастьян приделяет много внимания чистоте и ясности кода.

Реальных примеров приложений не так много. Но вы всегда можете задать вопрос в рассылке. Отвечают более чем оперативно.

С чего начать?
Создание приложения начинается с комманды "mojolicious generate app FastNotes", которая генерирует базовою структуру каталогов и файлы примеров.
  • lib - содержит весь наш код
  • log - содержит файлы логов. Если папку удалить, то логи буду выводиться на экран.
  • public - статические файлы
  • script - наше приложение
  • t - тесты templates - шаблоны для view
Запускаем наше приложение так "perl script/fast_notes daemon --reload"
lib/FastNotes.pm - это класс нашего приложения, наследуется от Mojolicious и содержит метод "startup".

Мы будем разбирать уже готовый прототип.
Вы можете взять его здесь - https://github.com/koorchik/FastNotes-Proto
Установка и запуск:
  1. Устанавливаем Mojolicious c Github
  2. Устанавливаем DBIx::Simple и SQL::Abstract
  3. Клонируем приложение с Github - git clone git://github.com/koorchik/FastNotes-Proto.git fastnotes
  4. perl fastnotes/script/fastnotes.pl daemon
  5. Запускаем браузер и открываем http://localhost:3000

RESTful Mojolicious
Наше приложение будет RESTful. Это важный момент и советую всем ознакомится с RESTful подходом.
Основная идея(в контексте веб-приложений) заключается в следующем.
  1. Есть множество ресурсов. И к каждому ресурсу мы можем приминить определенный набор дейтсвий, которые соответствуют методам HTTP протокола - GET, POST, PUT, DELETE. То есть такой себе CRUD положеный на HTTP. То есть мы рассматриваем HTTP не как транспортный протокол, а протокол уровня приложений. HTTP коды состояния тоже полезно использовать. Например, возвращать "201 Created" вместо "200 OK" при создании ресурса.
  2. Ресурс может иметь множество представлений. Например, json, xml, pdf. Клиент говорит, что ему подходит в хидере Accept
  3. Ресурс(или его представление) мы можем прочитать(GET), удалить(DELETE), записать (PUT) и создать (POST). Каждому ресурсу соответствует один контроллер. Как мы видим есть стандартный набор операций для работы с ресурсом, и каждый контроллер содержит стандартный набор методов.
  4. URI должен идентифицировать ресурс, воспринимайте его, как первичный ключ. То есть вы не должны передавать идентификатор ресурса в POST данных. Для работы с ресурсом используется один и тот же URI. Это позволяет организовать еффективное кеширование.
  5. Состояние клиента храните на клиенте(Signed Cookies тут очень кстати) либо создавайте ресурсы состояния(корзина товаров, например)
  6. Всегда следуйте этим правилам, но не ограничивайте себя ими и если необходимо, то нарушайте (только предварительно хорошо подумав).

Это вкратце, детальнее можно почитать:
Вот, например, как мы работаем с нашими заметками. Каждому роуту соответствует метод в контроллере.

МетодURIДействиеОписание
GET/notesNotes->index получить список заметок
POST/notesNotes->create создать заметку
GET/notes/23Notes->showполучить заметку с идентификатором "23"
PUT/notes/23Notes->updateобновить заметку или создать заметку с предзаданным идентификатором
DELETE/notes/23Notes->deleteудалить заметку "23"
И два дополнительных адреса:
GET/notes/newNotes->create_formполучить пустую форму для создания ресурса.
GET/notes/23/editNotes->update_formполучить форму для редактирования ресурса "23"

Всего 7 роутов и 7 методов. Я рекомендую использовать такие роуты. Аналогичные используются в Ruby on Rails.

Могут быть вложенные ресурсы, например, заметки пользователя "koorchik"
GET /users/koorchik/notes/1
POST /users/koorchik/notes/1

Вот еще буквально несколько интересных моментов:
  1. Имя контроллера всегда в множественном числе. /notes - контроллер Notes, /users/ - контроллер Users
  2. Не рекомендуется более двух уровней вложенности ресурсов.
  3. Допустим нам нужно сделать ссылку отчет в формате pdf по адресу /reports/myreport. Хидер "Accept:application/pdf" мы не можем, что же делать? Просто добавляем к адресу формат в котором мы хотим получить данные. /reports/myreport.pdf
  4. Нормально ли использовать QUERY_STRING в URL? Вполне! notes/search?user=koorchik&type=important&size=large - это вполне RESTful роут.
На клиенте я использую JQuery + JQuery-UI + JQuery.REST

Теперь преступим к разбору кода
Клас приложения - FastNotes
Советую предварительно прочитать https://github.com/kraih/mojo/wiki/Routes-for-non-lite-apps

package FastNotes;
use strict;
use warnings;
# Mojolicious является базовым классом для нашего приложения
use base 'Mojolicious'; 

# This method will run once at server start
sub startup {
    # $self - экземпляр класса FastNotes. Если в клас добавить 
    # метод DESTROY, то он вызовется при завершении приложения.
    my $self = shift;

    # Устанавливаем секретное слово для подписывания cookies.
    # По-умолчанию Mojolicious использует signed cookies для
    # хранения сессий. Это очень удобно и позволяет не хранить
    # состояние клиента на сервере. Но нужно учитывать, что
    # cokie передается при каждом запросе и ограничена 4кбайтами. 
    # Поскольку сессия хранится в coookie, то я Вам рекомендую 
    # хранить в cookie минимум данных. 
    # Принцип работы подписанных кук примерно следующий. Клиент 
    # не сможет подделать сессию если он не знает секретного 
    # слова, которым подписана кука.
    $self->secret('SomethingVerySecret');

    # Подключаем плагин Mojolicious::Plugin::JsonConfig. Считывает 
    # конфиг "fastnotes.json" с корневой папки приложения. 
    # По-умолчанию, имя конфига - (<имя исполняемого файла>.json).
    # Переменная $config содержит наш конфиг. После инициализации 
    # приложения мы можем получить доступ к конфигу используя
    # $self->stash('config').
    my $config = $self->plugin('json_config'); 

    # По умолчанию сессия действительна в течении часа с последнего 
    # запроса, но сдесь мы устанавливаем время жизни сессии равным неделе.
    $self->sessions->default_expiration(3600*24*7); 

    my $r = $self->routes;
    # Устанавливаем пространство имен для наших контроллеров.
    # По-умолчанию, Mojolicious будет их искать в lib/FastNotes, но
    # это неудобно поскольку у нас кроме контроллеров есть еще много
    # разных модулей. Я препочитаю, чтобы контроллеры находились
    # в lib/FastNotes/Controller
    $r->namespace('FastNotes::Controller');

    $r->route('/')                   ->to('auths#create_form')->name('auths_create_form');
    $r->route('/login')              ->to('auths#create')     ->name('auths_create');
    $r->route('/logout')             ->to('auths#delete')     ->name('auths_delete');
    $r->route('/signup')->via('get') ->to('users#create_form')->name('users_create_form');
    $r->route('/signup')->via('post')->to('users#create')     ->name('users_create');
    $r->route('/main')  ->via('get') ->to('users#show')       ->name('users_show');

    my $rn = $r->under('/notes')->to('auths#check'); # Проверка того, что юзер залогинен
    $rn->route                       ->via('get')   ->to('notes#index') ->name('notes_index');
    $rn->route                       ->via('post')  ->to('notes#create')->name('notes_create');
    $rn->route('/:id', id => qr/\d+/)->via('put')   ->to('notes#update')->name('notes_update');
    $rn->route('/:id', id => qr/\d+/)->via('delete')->to('notes#delete')->name('notes_delete');

    $r->route('/help')   ->to( cb => sub{ shift->render( template=>'help', format=>'html' ) } );

    # По поводу роутов :
    #   1. Контроллер Users не отвечает за аутентификацию. Для
    #      этого есть контроллер Auths(можно назвать Sessions).
    #   2. Желательно, чтобы каждый роут имел имя. Это имя потом
    #      используется повсюду в коде для того, чтобы можно было
    #      безболезнено менять URI.
    #   3. Если нужны более удобные пути, например, /login вместо
    #      /auths/create - используйте их. Пути должны говорить
    #      сами за себя :).
    #   4. under используется для того, чтобы проверить залогинен
    #      ли юзер
    #   5. Список роутов можно получить perl script/fastnotes routes

    # Load and init Model
    # При подключении модуля lib/Fastnotes/Model.pm подключаются
    # все модули с lib/Fastnotes/Model/*
    # Я использую везде "Mojo::Loader->load" вместо "use" для того,
    # чтобы работала опция --reload и для модели.
    Mojo::Loader->load('FastNotes::Model');
   
    # Используем базу по-умолчанию, если не определена база в конфиге.
    FastNotes::Model->init( $config->{db} || {
        dsn      => 'dbi:SQLite:dbname=' . $self->home->rel_dir('storage') . '/fastnotes.db',
        user     => '',
        password =>''
    });
}

1;


Контроллеры

В lib/FastNotes/Controller создаем 3 контроллера Users, Notes, Auths
В templates создаем папки по имени контроллеров. Нам нужны только auths и users. Шаблоны для notes нам не нужны, поскольку мы будем передавать все в формате JSON.

Если мы не вызвали в методе рентеринг $self->render, то Mojolicious автоматически отрендерит нам templates//
Может быть даже не быть метода в контроллере, но страничка все равно отрендериться.

Давайте рассмотрим для примера контроллер FastNotes::Controller::Auths;
У нас есть следующие роуты, которые указывают на контроллер, но для роута
"$r->route('/')->to('auths#create_form')->name('auths_create_form');" нет соответствуещего метода в контроллер. Все правильно и Mojolicious знает, что нужно отрендерить страничку auths/create_form.html.ep


package FastNotes::Controller::Auths;
use strict;
use warnings;
use v5.10;
use base 'Mojolicious::Controller';

sub create {
    Получаем объект контроллера (класс FastNotes::Controller::Auths)
    my ($self) = @_; 

    my $login    = $self->param('login'); 
    my $password = $self->param('password');

    my $user = FastNotes::Model::User->select({login => $login, password=>$password})->hash();

    if ( $login  && $user->{user_id} ) {
        # Сохраняем информацию про то, что юзер зашел в систему в сессию
        # и перенаправляем на главный экран пользователя используя имя роута.
        $self->session(
            user_id => $user->{user_id},
            login   => $user->{login}
        )->redirect_to('users_show'); # используем цепочку вызовов(chaining)

    }
    else {
        # Устанавливаем сообщение об ошибке.
        # Данные во flash автоматически очистяться при чтении.
        $self->flash( error => 'Wrong password or user does not exist!' )->redirect_to('auths_create_form');
    }
}

sub delete {
    # При редиректе используем имена роутов
    shift->session( user_id => '', login => '' )->redirect_to('auths_create_form'); 
}

sub check {
    shift->session('user_id') ? 1 : 0; 
}

1;

Шаблоны
В шаблонах я использую хелпер url_for "<имя роута>" для получения URL.
<form method='POST' action='<%= url_for "auths_create" %>'>
Mojolicious::Plugin::TagHelpers я не использую, поскольку считаю их избыточными и даже вредными :). Возможно я бы использовал хелпер form_for если бы он поддерживал защиту от XSRF. Остальное Вы найдете в документации.

Модель
Для модели я выделил свое пространство имен FastNotes::Model. На базе DBIx::Simple + SQL::Abstract сделал небольшую ORM.
Есть базовай класс FastNotes::Model::Base от которого мы наследуем FastNotes::Model::Note и FastNotes::Model::User, которые содержать по одному методу - table_name, возращает имя таблицы в которой хранить объекты.
Инициализируется модель в методе FastNotes->startup
FastNotes::Model::Base наследуется от Mojo::Base. Mojo::Base - некий упращенный аналог Class::Accessor, также Mojo::Base предоставляет конструктор "new".

PS: Приложение - лишь прототип. И это всего лишь моя попытка разобраться с новым фреймворком. Если есть какие-то полезные замечания по использованию Mojolicios - смело пишите в комментарии.

25 коммент.:

Unknown комментирует...

Вот у этих парней есть несколько примеров приложений на Mojolicious
https://github.com/vti
https://github.com/sharifulin

А так же полезных плагинов.

koorchik комментирует...

Спасибо, уже видел. Но они используют Mojolicious::Lite. А плагины - да, есть полезные.

Вот еще один парень - https://github.com/konstantinov .
И его полезный и свежий плагин - http://search.cpan.org/~dmitrynod/Mojolicious-Plugin-Recaptcha-0.11/lib/Mojolicious/Plugin/Recaptcha.pm , которым собираюсь воспользоваться.

Андрей комментирует...

Спасибо за статью, как раз сам хотел написать нечто подобное.

koorchik комментирует...

Рад был поделиться опытом.

Анонимный комментирует...

Только хотел поискать полезную инфу по фреймворку, как тут такое. Сенкс, очень своевременно.

Анонимный комментирует...

Было бы замечательно, если всякие описания "новых" фреймворков несли в себе сравнение например с уже имеющимися на рынке, плюсы и минусы... Каталист уже мервтоват для этого, а вот например с Yii, SYmfony из PHP и новыми Rails3 сравнить не мешало бы, а то бегло вообще не вижу плюсов в сравнении с другими

koorchik комментирует...

2mpak666:
Mojolicious еще в процессе написания и нет особого смысла тестировать его производительность. Кроме того, Mojolicious полностью зависит от доступных плагинов (больше плагинов - больше функционала), а их еще совсем немного. Даже стандартный функционал реализован плагинами.
С плюсов по сравнению с Yii, SYmfony из PHP - Mojolicious на Perl :) и нативно поддерживает REST.
А так, ИМХО, Rails3 - самый продвинутый на сегодняшний день фреймворк с обилием документации и плагинов.

chack комментирует...

А можете выложить весь рабочий исходник этого примера?

koorchik комментирует...

>А можете выложить весь рабочий исходник этого примера?
В посте ж описано, где лежат исходники и как их вытянуть :) :
https://github.com/koorchik/FastNotes-Proto

abra комментирует...

> Давайте рассмотрим для примера контроллер FastNotes::Controller::Auths; У нас есть следующие роуты, которые указывают на контроллер, но для роута "$r->route('/')->to('auths#create_form')->name('auths_create_form');" нет соответствуещего метода в контроллер. Все правильно и Mojolicious знает, что нужно отрендерить страничку auths/create_form.html.ep

а что делает следующая строка
$r->route('/login') ->to('auths#create') ->name('auths_create');
зачем нужен метод name()??

koorchik комментирует...

$r->route('/login') ->to('auths#create') ->name('auths_create');
зачем нужен метод name()??

Этот метод необязателен, я его использую для удобства. Метод name задает имя роута, которое потом можно использовать в шаблонах и в коде.
Например,
$self->redirect_to('auths_create'); #в контроллере
<%= url_for 'auths_create' %> # в шаблоне
<%= form_for 'auths_create' %> # в шаблоне

Такой подход позволяет безболезненно менять URL. Например, заменив "...route('/login')..." на "...route('/signin')..." все останется работоспособным.

abra комментирует...

Хотел бы еще спросить :) Не могу разобраться с методом bridge()? Как он работает в вашем коде? В оф. доке не разобрался http://mojolicio.us/perldoc?Mojolicious/Guides/Routing#Bridges :(

koorchik комментирует...

bridge используется для проверки того, залогинен ли юзер. Это что-то вроде вложенных роутов. Мы обрабатываем URL по частям. Сначала совпадает первая часть - "/notes/" с bridge("/notes"), а затем следующая. Если метод auths#check нам вернул ложь, то цепочка обрывается.

Например, мы хотим, чтобы все URL, которые начинаются на admin были доступны только для залогиненых пользователей.
"/admin/posts/123"

Мы для этого добавлем bridge("/admin") к нашим роутам.
Вместо
$r->route('/admin/posts/:id')->to('posts#show');
пишем
$r->bridge('/admin')->to('auths#check')->route('/posts/:id')->to('posts#show');

И в auths#check делаем проверку на то, залогинен ли юзер. Если он не залогинен, то мы можем сделать редирект на экран входа, а с метода возвращаем 0 для того, чтобы прервать цепочку.

Цепочки могут быть любой длины

abra комментирует...

2koorchik спасибо! теперь понятно :)

Unknown комментирует...

Хорошая статья, спасибо!

Анонимный комментирует...

А как в модель протянуть функции из контроллера? И надо ли это вообще?
Лично меня интересует доступ к stash что бы переводить некоторые сообщения

koorchik комментирует...

> А как в модель протянуть функции из контроллера? И надо ли это вообще?
Такого не надо делать,это может вызвать ряд проблем в будущем.

Передавайте в модель значения со stash, как параметры методов.

Придерживайтесь MVC:
1. Model - полностью вся логика приложения находится здесь. Не должно быть завимостей от того,где эти модули будут использаваться. То есть Вы должны иметь возможно использовать модель в консольных скрипта, в веб приложении, в другом веб-фреймворке и так далее.
2. View - это по сути набор шаблонов(если рассматривать веб). Тут только визуализация данных, никакой обработки и никакого доступа к модели.
3. Controller - содержит код, который склеивает Model и View. Например, получает список контактов определенного пользователя с Model, затем преобразовывает этот список контактов в формат понятный для View.

Анонимный комментирует...

Спасибо, стало как то яснее!

Я пока сделал дополнительную функцию stash в FastNotes::Model, и в контроллере при вызове какой либо функции делаю
FastNotes::Model->set_stash
а в модели get_stash.

Как вам такой вариант? В Модели я уже проверяю доступен ли stash и если доступен делаю с его помощью перевод.

Еще хотел поинтересоваться, вы не подскажете есть ли какой то готовый модуль что бы кешировать запросы в DBIx::Simple через memcached. В принципе там нет ничего особо сложного написать свой, но если есть уже готовое.

koorchik комментирует...

> Я пока сделал дополнительную функцию stash в FastNotes::Model, и в контроллере при вызове какой либо функции делаю
FastNotes::Model->set_stash
а в модели get_stash. Как вам такой вариант?

Если попытаться использовать модель отдельно от веб-части, где Вы возьмете stash? Я бы в модели вообще отказался от использования понятия stash. Такие сущности, как Stash, Cookies, Routes не имеют отношения к модели. Если Вы опишите зачем Вам stash в модели, то возможно я предложу другой вариант решения проблемы.

>Еще хотел поинтересоваться, вы не подскажете есть ли какой то готовый модуль что бы кешировать запросы в DBIx::Simple через memcached. В принципе там нет ничего особо сложного написать свой, но если есть уже готовое.
Основная проблема при кешировании - это очистка кеша. И для чистого SQL Вы вряд ли найдете готовое решение(разве что самое примитивное). Кроме того, кеширование всегда можно добавить позже, когда количество посетителей системы будет измерятся тысячами в сутки.

PS: Если планируется хоть несколько сложный сайт, то я советовал бы обратить внимание на Rose::DB::Object для реализации модели. Эта ORM имеет хорошее быстродействие и значительно облегчает разработку.

Анонимный комментирует...

Мне нужен stash что бы переводить некоторые сообщение которые хранит та или иная модель.
А откуда еще мне получить текущий язык и доступ к i18n?

Если не будет доступен stash переменная идет возвращается без изменений.

Сайт вообще планируется сложный, я переписываю старый проект с HTML::Mason + DBIx::SearchBuilder.

В Rose я так понимаю уже идет какой то модуль кеширования?
Спасибо!

Анонимный комментирует...

Да и посещаемость на старом движке уже переваливает за тысячи, потому кеширование ой как надо.

koorchik комментирует...

> Мне нужен stash что бы переводить некоторые сообщение которые хранит та или иная модель. А откуда еще мне получить текущий язык и доступ к i18n?
1. Дейстительно ли модель должна отвечать за перевод? Обычно перевод происходит снаружи( в шаблонах, в контроллере ).
2. Если нужно все таки осуществлять перевод в модели(мне никогда не требовалось), то можно передавать не stash, а язык (как-то типа так Model->new(lang=>'RU') или Model->lang('RU') ... вариантов много). Локализатор можно инстанцировать внутри модели либо тоже передать параметром. Так зависимости будут явными и не привязаны к вебчасти.

> Сайт вообще планируется сложный, я переписываю старый проект с HTML::Mason + DBIx::SearchBuilder.В Rose я так понимаю уже идет какой то модуль кеширования?
В Rose есть кеширование, но очень ограниченное. Я бы прикручивал кеширование на более высоком уровне самостоятельно.

Анонимный комментирует...

Понял, благодарю.
Над переводом задумаюсь, на самом деле да, от перевода в модели надо уйти.

Людмила комментирует...

>$r->bridge('/admin')->to('auths#check')->route('/posts/:id')->to('posts#show');

Не знаю, было ли это раньше, но для этих целей, наверное, лучше использовать ->under. Что скажете?

koorchik комментирует...

Да, все верно. Пост писался давно, bridge уже переименовали в under, на гитхабе уже давно under в исходниках. Пост обновил. Спасибо!

Отправить комментарий

Не забудьте добавить себя в постоянные читатели и включить уведомления о новых комментариях, либо воспользуйтесь RSS каналом ;)