Документация Mojolicious: Потерянные Главы

в 19:22, , рубрики: mojolicious, perl, web-разработка, Веб-разработка, документация, Программирование, метки: , , ,

Mojolicious — восхитительный современный веб-фреймворк для Perl. Из недостатков я могу назвать только два: политика в отношении обратной совместимости и документация.

Содержание

  1. Недостатки
  2. Роутинг: внутреннее устройство
  3. Роутинг: настройка
  4. Параметры HTTP-запроса
  5. Парсинг
  6. Tips & Tricks
    1. Поддержка неблокирующих приложений в режиме CGI
    2. Как работает Mojo::UserAgent при тестировании своего приложения
    3. ojo и Mojolicious::Lite
    4. Переменные окружения

Недостатки

В официальном FAQ написано: "… we will always deprecate a feature before removing or changing it in incompatible ways between major releases … as long as you are not using anything marked experimental, untested or undocumented, you can always count on backwards compatibility …". Для начала, вторая фраза противоречит первой. Далее, вот цитата из Guides::Contributing «Features may only be changed in a major release or after being deprecated for at least 3 months.». Честно говоря, 3 месяца это и так смешной срок когда речь идёт об обратной совместимости, но похоже что даже этот срок соблюдается не всегда (поддержку «X-Forwarded-HTTPS» сделали deprecated два месяца назад, а удалили месяц назад — да, это был «major release» поэтому формально правила не нарушены, но общее отношение к обратной совместимости вполне показательно). Как много разработчиков обновляет фреймворк чаще чем раз в 3 месяца, да ещё и при этом тщательно вычитывает Changes или логи своего приложения на предмет deprecated-предупреждений? При этом, в течении последнего года было deprecated примерно 20 функций/фич. На практике, конечно, всё не так плохо, как это звучит — что-то ломается не так уж часто (лично меня за последний год коснулась только замена $app->secret() на $app->secrets()). Но факт остаётся фактом — обратную совместимость ломают, ломают часто, причём без по-настоящему веских причин: например, в случае с secret() абсолютно ничего не мешало добавить в код

sub secret { shift->secrets([shift]) }

либо просто добавить поддержку дополнительных параметров в secret() вместо добавления новой функции secrets() реализовав нужную фичу вообще не ломая совместимость.

Что касается документации, то многие считают её отличной, даже одним из серьёзных достоинств Mojolicious, но никак не недостатком. Проблема с документацией в том, что она вся сосредоточена на примерах. Это реально круто, когда ты начинаешь изучать фреймворк. Это экономит кучу времени, когда тебе нужно сделать фичу и ты быстро гуглишь пример аналогичной фичи в официальных guides. Но как только ты выходишь за рамки стандартных задач и возникает необходимость понять, как что-то устроено идеологически или архитектурно, какие конкретно параметры может принимать эта функция и что конкретно она может возвращать в разных ситуациях — выясняется, что для многих модулей Mojolicious такая документация отсутствует в принципе. И не потому, что эта информация относится к «недокументированным возможностям» — почти всё это мельком упоминается здесь и там в разных примерах, а значит считается «документированным». Нередко есть несколько способов получить доступ к определённым данным (параметры запроса, тело ответа, etc.) но не описано чем они друг от друга отличаются и в каких ситуациях правильнее пользоваться какими способами. И последнее — алфавитный порядок функций в доке, серьёзно?! Нет, я понимаю, все люди разные и кому-то наверняка это удобно, но многим всё-таки на порядок проще воспринимать документацию в которой функции сгруппированы по задачам. (Хотя в коде, особенно при чтении его через браузер, где не так удобно пользоваться поиском как в Vim, алфавитный порядок функций неожиданно оказался довольно удобным — кроме new/DESTROY/AUTOLOAD — их всё-таки лучше размещать в начале.) В результате, чтобы разобраться приходится вычитывать код (некоторые предпочитают вместо этого смотреть тесты!), что не так просто — во-первых он не является эталоном читабельности: автор любит использовать фишки перла, которые позволяют писать код компактно (и нередко такой код быстрее работает), но читабельность это ухудшает; во-вторых активное использование и наследования и обмена событиями между объектами усложняет понимание того, что и как происходит внутри 104 классов, из которых состоит Mojolicious-5.

С проблемой обратной совместимости мы мало что можем сделать (хотя, наверное, можно сделать плагин к Mojolicious, который будет её эмулировать по мере возможности). Зато вторую проблему решить не сложно — недостающую документацию можно написать самостоятельно. По мере изучения Mojolicious я планирую описывать некоторые вещи, которые, по-хорошему, должны быть в официальной документации, отсюда и название этой статьи.

$self

В документации Mojolicious часто используют $self, что не добавляет читабельности — в фреймворке слишком много классов, и далеко не всегда глядя на $self легко сообразить какого класса этот объект в данном примере. Поэтому я в примерах вместо $self буду использовать:

$app    # YourApp → Mojolicious
$r      # Mojolicious::Routes ($app->routes)
$c      # YourApp::SomeController → Mojolicious::Controller
$ua     # Mojo::UserAgent

Роутинг: внутреннее устройство

Первое, что необходимо понимать про устройство роутинга в Mojolicious — он реализован как дерево узлов, причём структура этого дерева (почти) никак не связана с иерархией пути в url. Рассмотрим пример:

$r->get("/a/b/c/d")     ->to(text=>"1");
$ab = $r->route("/a")->route("/b");
$ab->get("/c")          ->to(text=>"2-1");
$ab->get("/c/d")        ->to(text=>"2-2");
$r->get("/a/b/c/d/e")   ->to(text=>"3");

В результате будет построено вот такое дерево:

$r                    {}
 ├─/a/b/c/d           {text=>"1"}
 ├─/a───/b─┬─/c       {text=>"2-1"}
 │         └─/c/d     {text=>"2-2"}
 └─/a/b/c/d/e         {text=>"3"}

И вот как оно будет работать:

GET /a/b            => 404 Not Found
GET /a/b/c          => "2-1"
GET /a/b/c/d        => "1"
GET /a/b/c/d/e      => "3"
GET /a/b/c/d/e/f    => 404 Not Found

Как можно догадаться, дерево просматривается последовательно (вглубь), до первого успешного совпадения с текущим запросом — так что если у вас в определении роутинга есть узлы, совпадающие с одинаковыми запросами, то внимательно следите за тем, в каком месте дерева они находятся — порядок, в котором они совпадают с запросами, может не совпадать с порядком, в котором они написаны в коде.

Второе, что необходимо понимать — только листья (терминальные узлы) дерева будут обрабатывать входящие запросы, все внутренние узлы запросы обрабатывать не будут, вне зависимости от того, как именно они были созданы (через route(), bridge() или обычный get(), etc.) и задан ли для них обработчик запроса ("controller#action" или {cb=>&handler}, etc.).

Например, создадим дерево используя get():

$r->get("a", {text=>"A"})->get("b", {text=>"B"});

GET /a              => 404 Not Found
GET /a/b            => "B"

Или вообще не будем создавать никаких узлов, а вместо этого просто настроим уже существующий корневой узел:

$app->routes->to(text=>"wow");

GET /               => "wow"

Единственный случай, когда обработчик заданный внутреннему узлу используется — если этот узел является bridge. Такие узлы создаются через bridge() и under(), либо можно существующий узел сделать bridge вызвав inline(1). После определения терминального узла, который должен обрабатывать текущий запрос, будут последовательно вызваны обработчики всех bridge-узлов на пути от корня дерева к терминальному. Эти обработчики должны вернуть истину или ложь (можно даже асинхронно) — если они вернут ложь, то последующие обработчики, включая обработчик терминального узла, вызваны не будут.

Далее, любой узел дерева может содержать следующую информацию:

  1. шаблон пути (pattern), может включать placeholders
    "/something/:name/:id"
    
  2. ограничения (constraints) для допустимых значений placeholders
    name => ["alex","nick"], id => qr/^d+$/
    
    • особое ограничение: можно ли к шаблону пути добавлять расширения и какие именно
      format => 0 или format => ["json","xml"]
      

  3. условия (conditions) — любые функции которые будут вызваны после совпадения пути с шаблоном чтобы выполнить любые дополнительные проверки (напр. http-заголовков) и вернуть истину/ложь чтобы разрешить или запретить использовать этот узел для обработки текущего запроса
    agent => qr/Firefox/
    
  4. параметры по умолчанию (defaults) — здесь задаются и управляющие параметры (controller/action/cb/…), и значения по умолчанию для placeholders (делающие эти placeholders опциональными) и любые другие значения которые должны оказаться в $c->stash() при обработке запроса
  5. явно заданное имя этого узла — напр. для использования в url_for
    • если его не задать — оно будет сгенерировано автоматически

Все эти данные (кроме имени узла) «наследуются» вложенными узлами (если они их не переопределят явно), что позволяет создавать и использовать внутренние узлы исключительно для установки всех эти данных по умолчанию для вложенных узлов. Кстати, узлы можно создавать не задавая им вообще никаких параметров, даже шаблон пути — в этом случае он будет такой же, как у родительского узла.

# Установим defaults для всех на корневом узле.
$r->to("users#");
# Создадим узел для /resource с ограничением формата.
$b = $r->route("/resource", format => 0);
# Добавляя в $b вложенный узел мы делаем $b внутренним узлом и теперь
# он больше не может сам обрабатывать запросы к /resource.
# Поскольку мы вызвали get() без указания шаблона пути, то он будет
# срабатывать на тот же путь, что и его родитель, т.е. на /resource.
$b->get()->over(agent=>qr/Firefox/)->to("#ff_only");
$b->get()->to("#not_ff");

Насколько я понимаю, разницы между установкой значений по умолчанию через $app->defaults и через корневой узел $app->routes->to скорее всего никакой нет (возможно, какие-то хуки срабатывают совсем до роутинга, и тогда в них значения из $app->defaults будут доступны а из корневого узла могут быть не доступны).

Есть ещё несколько нюансов, например: bridge не обрабатывает запросы даже если он является терминальным узлом, корневой узел немного иначе обрабатывает значение format чем все остальные узлы… но я не думаю, что это важно для общего понимания, поэтому не буду вдаваться детально.

Я пока не разбирался как работает подключение отдельного Mojolicious-приложения к текущему через $r->any("/path")->to(app=>$otherapp), возможно там есть дополнительные нюансы.

Роутинг: настройка

Есть отличие между Mojolicious и Mojolicious::Lite — в Mojolicious функции bridge и under делают примерно одно и то же, но в Mojolicious::Lite under (вместе с group) работает несколько иначе. Я здесь буду описывать функции Mojolicious (точнее, Mojolicious::Routes::Route).

Все параметры всех функций не обязательные (кроме over(), to() и via() — они при вызове без параметров возвращают текущее значение).

  • Простые низкоуровневые функции:
    • $r->route() создаёт новый узел, параметры:
      • шаблон пути (первый скаляр при нечётном количестве параметров)
      • ограничения, включая format (пары параметров)

    • $r->bridge() то же что и $r->route() плюс $r->inline(1)
    • $r->via() устанавливает HTTP-метод(ы)
      • метод(ы) (список или ссылка на список)

    • $r->over() устанавливает условия
      • условия (пары параметров либо ссылка на массив пар параметров)

    • $r->to() устанавливает параметры по умолчанию
      • обработчик (приложение или пакет, controller и/или action) (первый скаляр при нечётном количестве параметров)
      • параметры по умолчанию (пары параметров либо ссылка на хеш)

  • Навороченные комбайны для ленивых:
    • $r->under() создаёт bridge
      • все параметры как у get()

    • $r->any() создаёт узел для любых HTTP-методов
      • первым параметром можно задать метод(ы) (ссылка на массив)
      • остальные параметры как у get()

    • $r->get() создаёт узел для HTTP-метода GET
      • шаблон пути (первый из скалярных параметров)
      • условие (скаляр плюс следующий за ним параметр)
      • имя узла (скаляр являющийся последним параметром)
      • функция-обработчик (ссылка на функцию; задаёт значение параметра по умолчанию «cb»)
      • ограничения, включая format (ссылка на массив)
      • параметры по умолчанию (ссылка на хеш)

    • $r->post() создаёт узел для HTTP-метода POST
      • все параметры как у get()

    • $r->put() создаёт узел для HTTP-метода PUT
      • все параметры как у get()

    • $r->delete() создаёт узел для HTTP-метода DELETE
      • все параметры как у get()

    • $r->patch() создаёт узел для HTTP-метода PATCH
      • все параметры как у get()

    • $r->options() создаёт узел для HTTP-метода OPTIONS
      • все параметры как у get()

# вот какую фигню может переварить Mojolicious
$r->get("/users/:id",
    [ format => 0 ],
    agent => qr/Firefox/,
    { id => -1, controller => "users" },
    [ id => qr/^d+$/ ],
    headers => { "X-Secret" => "letmeit" },
    &cb,
    { action => "list" },
    "my_cool_route",
);
# а вот как это можно записать без get()
$r->route("/users/:id", id => qr/^d+$/, format => 0)
    ->via("GET")
    ->over(agent => qr/Firefox/, headers => { "X-Secret" => "letmeit" })
    ->to(id => -1, controller => "users", action => "list", cb => &cb)
    ->name("my_cool_route");

Параметры HTTP-запроса

Способов добраться до параметров запроса существует не просто много, а очень много. При этом далеко не всеми их них стоит пользоваться — в некоторых случаях параметры полученные из разных мест смешиваются вместе, и понять что откуда берётся становится нереально.

В Mojolicious есть 4 типа параметров:

  1. GET — полученные из query string в url, причём метод HTTP запроса может быть любым — GET, POST, etc.
  2. POST — полученные из тела POST-запроса типа application/x-www-form-urlencoded либо типа multipart/form-data — но в этом случае берутся только обычные параметры, кроме файлов
  3. UPLOAD — полученные из тела POST-запроса типа multipart/form-data файлы
  4. ROUTE — значения вырезанные из пути url используя placeholders в роутинге, исключая зарезервированные для stash

Далее, надо отметить, что один и тот же параметр может быть передан несколько раз, более того, он может быть передан несколько раз каждым из способов — GET, POST, UPLOAD, да и при определении роутинга можно несколько раз упомянуть один и тот же placeholder. Для GET, POST и UPLOAD сохраняются все переданные значения одного параметра, но для ROUTE используется только одно, последнее значение, если один placeholder указан несколько раз.

Чаще всего в примерах упоминается $c->param — посмотрим, откуда берутся значения, которые возвращает эта функция:

  • scalar $c->param() — возвращает undef
  • @names = $c->param() — возвращает имена всех GET, POST, UPLOAD и ROUTE параметров
  • $value = $c->param("name") — возвращает:
    1. последнее значение placeholder «name» если оно есть, иначе
    2. первое значение UPLOAD «name» если оно есть, иначе
    3. первое значение POST «name» если оно есть, иначе
    4. первое значение GET «name» если оно есть, иначе
    5. undef

  • @values = $c->param("name") — возвращает:
    1. последнее значение placeholder «name» если оно есть, иначе
    2. все значения UPLOAD «name» если они есть, иначе
    3. все значения POST «name» после которых все значения GET «name» если они есть, иначе
    4. ()

Лично я предпочитаю писать максимально ясный код, и мне не нравятся такие «магические» функции, которые параметр-то вернут, но при этом совершенно неизвестно, откуда он берётся. Конечно, в некоторых случаях нужно не обращать внимания на то, переданы параметры GET-ом или POST-ом, но то, что делает $c->param — это уже за гранью добра и зла. Всё хорошо в меру, даже магические функции-ленивчики создающие вау-фактор, которые так любят реализовывать в Mojolicious.

Вот список функций, которыми рекомендуется ограничиться для доступа к параметрам HTTP-запроса:

  • Получить значение placeholder из пути url:
    $value = $c->stash( "name" )              # ROUTE
    

  • Получить имена и все значения всех параметров как HASHREF (значения ключей в хеше будут либо скалярами — если у параметра одно значение, либо ссылкой на массив скаляров — если у параметра несколько значений):
    $params = $c->req->params->to_hash        # сначала POST, потом GET
    $params = $c->req->query_params->to_hash  # GET
    $params = $c->req->body_params->to_hash   # POST
    

  • Получить имена всех параметров:
    @names = $c->req->params->param           # POST и GET
    @names = $c->req->query_params->param     # GET
    @names = $c->req->body_params->param      # POST
    

  • Получить все значения (в списковом контексте) либо первое значение (в скалярном контексте) одного параметра:
    $c->req->params->param( "name" )          # сначала POST, потом GET
    $c->req->query_params->param( "name" )    # GET
    $c->req->body_params->param( "name" )     # POST
    $c->req->upload( "name" )                 # UPLOAD
    

Такой подход обеспечит ясность и единообразие кода. Но, для полноты картины, вот список оставшихся функций:

  • Альтернативные варианты вызова вышеупомянутых функций:
    • $c->req->param это то же самое, что и $c->req->params->param
    • $c->req->url->query это то же самое, что и $c->req->query_params

  • Вместо to_hash можно использовать params — она возвращает ссылку на массив где последовательно идут имена и значения всех параметров, при этом одно имя может встретиться несколько раз но зато все значения это скаляры.
  • Так же есть аналогичная params функция $c->req->uploads — она возвращает ссылку на массив всех залитых файлов.

Парсинг

Это не совсем из моего словаря, но другое описание подобрать не получается: в плане парсинга скачанных страничек моджо просто няшка! Документация по этой части Mojo значительно лучше. Тем не менее, и здесь есть что добавить.

Дело в том, что Mojo это не просто серверный веб-фреймворк, а веб-фреймворк вообще — и для сервера, и для клиента. Поэтому модули, которые реализуют формат HTTP-сообщений, используются и сервером, и клиентом — а у них очень разные потребности по обработке этих сообщений. В результате, когда нужно что-то сделать и лезешь в документацию, тебя интересует в этот момент либо серверная функциональность, либо клиентская — а видишь ты мало того что и то, и другое, так ещё и тщательно отсортированное в алфавитном порядке. В результате найти нужную функцию достаточно сложно, и возникает потребность в наглядной шпаргалке, где будут функции только для клиента или только для сервера, желательно сгруппированные по какому-то адекватному критерию.

В результате получилась следующая табличка. Это первая альфа-версия :) так что если что-то не понятно или есть идеи по улучшению — пишите, допилим совместными усилиями. Несколько замечаний:

  • Даже при ошибке выкачки $tx->res будет доступен (с заглушкой 404).
  • У дерева в $dom есть корневой узел, а узел с тегом html (если он был в скачанной страничке) является потомком корневого узла. Из-за этого to_string и content на корневом узле возвращают одно и то же.
  • При обращении к несуществующим элементам (или напр. если ожидалась коллекция а найден один элемент через $dom->child_tag_name и вызывается метод коллекции) перл кинет исключение «нет такого метода» — иными словами практически всегда парсер необходимо заключать в eval.
  • Очень просто нечаянно получить коллекцию коллекций вместо коллекции узлов, или коллекцию, в которой часть элементов пустые — тут помогут методы коллекций flatten и compact.
  • Параметр "*" означает строку с CSS-selector-ом.

$tx = $ua->get($url);           # Mojo::Transaction::HTTP → Mojo::Transaction
$tx->error                      # undef или {message=>'…',…}
$tx->success                    # undef или $tx->res
$tx->req                        # Mojo::Message::Request  → Mojo::Message
$tx->res                        # Mojo::Message::Response → Mojo::Message
$tx->redirects                  # [ Mojo::Transaction::HTTP, … ]

$res = $tx->res;                # Mojo::Message::Response → Mojo::Message
$res->error                     # undef или {message=>'Parse error',…}
$res->to_string                 # "…" (headers+content)
$res->is_status_class(200);     # bool
$res->code                      # 404
$res->message                   # "Not Found"
$res->headers                   # Mojo::Headers
$res->cookies                   # [ Mojo::Cookie::Response, … ]
$res->cookie("name")            # Mojo::Cookie::Response → Mojo::Cookie
$res->body                      # "…"
$res->text                      # "…" (decoded body using charset)
$res->dom                       # Mojo::DOM
$res->json                      # Mojo::JSON

$headers = $res->headers;       # Mojo::Headers
$headers->names                 # [ "Content-Type", "Server", … ]
$headers->to_hash               # { "Content-Type" => "…", … }
$headers->header("Server")      # "…"
$headers->$standard_header_name # "…" (shortcuts for useful headers)

$dom = $res->dom;               # Mojo::DOM
$dom->to_string                 # "…" (этот узел, включая содержимое)
$dom->content                   # "…" (содержимое этого узла)
$dom->node                      # "…" (тип узла: root,tag,text,comment,…)
$dom->type                      # "…" или "" (название тега)
$dom->attr                      # {name=>"val",…}
$dom->attr("name")              # "val"
$dom->{name}                    # синоним $dom->attr("name")
$dom->all_text                  # "…" (из всех узлов)
$dom->all_text(0)               # "…" (из всех узлов, не трогая пробелы)
$dom->text                      # "…" (из этого узла)
$dom->text(0)                   # "…" (из этого узла, не трогая пробелы)
$dom->root                      # Mojo::DOM (корневой узел)
$dom->parent                    # Mojo::DOM или undef (узел-родитель)
$dom->next                      # Mojo::DOM или undef (следующий тег-брат)
$dom->next_sibling              # Mojo::DOM или undef (следующий узел-брат)
$dom->previous                  # Mojo::DOM или undef (предыдущий тег-брат)
$dom->previous_sibling          # Mojo::DOM или undef (предыдущий узел-брат)
$dom->match("*")                # Mojo::DOM или undef (тест этого узла как тега)
$dom->at("*")                   # Mojo::DOM или undef (первый подходящий тег)
$dom->find("*")                 # Mojo::Collection (указанные теги)
$dom->ancestors                 # Mojo::Collection (теги-родители)
$dom->ancestors("*")            # Mojo::Collection (указанные теги-родители)
$dom->siblings                  # Mojo::Collection (теги-братья)
$dom->siblings("*")             # Mojo::Collection (указанные теги-братья)
$dom->children                  # Mojo::Collection (теги-дети)
$dom->children("*")             # Mojo::Collection (указанные теги-дети)
$dom->$child_tag                # Mojo::DOM или Mojo::Collection
$dom->all_contents              # Mojo::Collection (все узлы)
$dom->contents                  # Mojo::Collection (все узлы-дети)
$dom->[0]                       # синоним $dom->contents->[0]

Tips & Tricks

Поддержка неблокирующих приложений в режиме CGI

Если немного модифицировать скрипт запускающий Mojolicious-приложение можно обеспечить поддержку неблокирующего кода даже при работе в режиме cgi: https://gist.github.com/powerman/5456484

Как работает Mojo::UserAgent при тестировании своего приложения

Если вы задумывались, каким образом в тестах $ua посылает запросы вашему приложению и получает ответы, не запуская TCP-сервер на каком-то порту — ответ прост: можно глобально задать любое приложение $app, и все объекты Mojo::UserAgent, не важно, созданные до этого или после, будут выполнять запросы к url, в которых не указаны протокол и хост, через этот $app.

Обычно это «просто работает» благодаря тому, что данную операцию выполняет Mojolicious::Lite. Но если ваш тест не использует этот модуль, да ещё и в коде где-то вы создаёте свои объекты Mojo::UserAgent — всё что «работало само» внезапно перестанет работать. Починить можно вот так:

Mojo::UserAgent::Server->app($app);

ojo и Mojolicious::Lite

Документация по ojo забыла упомянуть, что помимо описанных функций ojo доступны функции Mojolicious::Lite, что упрощает эксперименты с Mojolicious:

$ perl -Mojo -E 'get "/", {text=>"wown"}; app->start' get /
wow

Переменные окружения

Mojolicious поддерживает десятки разных переменных окружения с именами начинающимися на MOJO_ — я не вижу смысла описывать все, т.к. подозреваю что список может меняться между версиями Mojolicious, но парочку стоит упомянуть:

  • MOJO_MODE — большинство остальных переменных окружения влияет на что-то одно, но эта влияет на разные вещи, что документировано опять же в разных местах, так что просто соберём всё вместе:
    • устанавливает $app->mode
    • определяет имя лог-файла
    • определяет какой вариант шаблонов exception и not_found будет использоваться (имя файла будет включать значение MOJO_MODE, напр. «exception.$mode.html.ep»)
    • $app->log->level по умолчанию изменяется с «debug» на «info» если значение MOJO_MODE не «development»
    • если существует, функция ${mode}_mode() будет вызвана перед startup() (не уверен, считается ли эта фича документированной).

  • MOJO_LOG_LEVEL — часто приходится вручную выставлять её в «warn» в тестах использующих Mojolicious::Lite, чтобы вывод теста не замусоривать сообщениями лога.

Автор: powerman

Источник

Поделиться

* - обязательные к заполнению поля