Введение В Разработку Веб-Приложений На Psgi/Plack. Часть 2

С разрешения автора и главного редактора журнала PragmaticPerl.com продолжаю публикацию серии статей.

Оригинал статьи здесь.

Продолжение статьи о PSGI/Plack. Более подробно рассмотрены Plack::Builder, а также Plack::Middleware. В прошлой статье мы рассмотрели спецификацию PSGI, как она появилась и почему ее следует использовать.

Мы рассмотрели Plack — реализацию PSGI, его основные компоненты и написали простой API, выполняющий возложенные на него задачи, а также кратко рассмотрели основные серверы PSGI. Во второй части статьи мы рассмотрим следующие моменты:

  • Plack::Builder — это мощный маршрутизатор и многое другое.

  • Plack::Middleware — расширяем свои возможности с помощью «слоев».

Мы по-прежнему используем Starman, который является сервером предварительного разветвления (использует предварительно разветвленную модель процесса).



Более пристальный взгляд на Plack::Builder

В предыдущей статье мы кратко рассмотрели Plack::Builder. Теперь пришло время рассмотреть это более подробно.

Решение рассмотреть Plack::Builder вместе с Plack::Middleware очень логично, поскольку они очень тесно связаны.

Если рассматривать эти два компонента в разных статьях, обе статьи будут содержать перекрестные ссылки друг на друга, что не очень удобно в журнальном формате.

Базовый дизайн Plack::Builder выглядит следующим образом:

  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
   

builder { mount '/foo' => builder { $bar }; }

Эта конструкция сообщает нам, что приложение PSGI ($bar) будет расположено в /foo. То, что мы завернули в конструктор, должно быть ссылкой на функцию, иначе мы можем получить такую ошибку: Невозможно использовать строку («глупая строка») в качестве ссылки на подпрограмму, пока в строке 71 /usr/local/share/perl/5.14.2/Plack/App/URLMap.pm используются «строгие ссылки».

Маршруты могут быть вложенными, например:

builder { mount '/foo' => builder { mount '/bar' => builder { $bar; }; mount '/baz' => builder { $baz; }; mount '/' => builder { $foo; }; }; };

Эта запись означает, что приложение $foo будет расположено в /foo, приложение $bar будет расположено в /foo/bar, а приложение $baz будет расположено в /foo/baz соответственно.

Однако никто не мешает записать предыдущую запись в следующем виде:

builder { mount '/foo/bar' => builder { $bar }; mount '/foo/baz' => builder { $baz }; mount '/foo/' => builder { $foo }; };

Обе записи эквивалентны и выполняют одну и ту же задачу, но первая выглядит проще и понятнее.

Plack::Builder можно использовать в объектно-ориентированном стиле, но лично я предпочитаю использовать его процедурно.

Использование Plack::Builder в объектно-ориентированной форме выглядит следующим образом:

my $builder = Plack::Builder->new; $builder->mount('/foo' => $foo_app); $builder->mount('/' => $root_app); $builder->to_app;

Эта запись эквивалентна:

builder { mount '/foo' => builder { $app; }; mount '/' => builder { $app2; }; };

Какой метод использовать – вопрос сугубо индивидуальный.

Мы вернемся к Plack::Builder после рассмотрения Plack::Middleware.

Плак::Промежуточное ПО

Plack::Middleware — это базовый класс для написания, как говорит нам CPAN, «простого в использовании промежуточного программного обеспечения PSGI».

Для чего это? Давайте рассмотрим пример реализации определенного API. Представим, что код нашего приложения выглядит так:

my $api_app = sub { my $env = shift; my $req = Plack::Request->new($env); my $res = $req->new_response(200); my $params = $req->parameters(); if ($params->{string} && $params->{string} eq 'data') { $res->body('ok'); } else { $res->body('not ok'); } return $res->finalize(); }; my $main_app = builder { mount "/" => builder { $api_app }; }

Это приложение прекрасно работает, но теперь представьте, что вам вдруг понадобится принимать данные только в том случае, если они переданы методом POST. Тривиальное решение — сделать наше приложение таким:

my $api_app = sub { my $env = shift; my $req = Plack::Request->new($env); my $res = $req->new_response(200); my $params = $req->parameters(); if ($req->method() ne 'POST') { $res->status(403); $res->body('Method not allowed'); return $res->finalize(); } if ($params->{string} && $params->{string} eq 'data') { $res->body('ok'); } else { $res->body('not ok'); } return $res->finalize(); };

Для решения проблемы потребовалось всего 4 строчки.

Теперь представим, что нам нужно сделать еще одно приложение, которое также должно принимать данные, отправленные только методом POST. Что мы делаем? Напишите это условие в каждом? Это не вариант по нескольким причинам:

  • Увеличивается объём кода и, как следствие, его энтропия (простое лучше сложного).

  • Больше шансов совершить ошибку (человеческий фактор).

  • Если мы передаем проект другому программисту, он может забыть и сделать что-то не так (человеческий фактор).

Итак, сформулируем задачу.

Мы не можем заставить все наши приложения одновременно приобретать определенные свойства, не меняя их код. Или можем? Механизм промежуточного программного обеспечения отлично подходит для обеспечения сквозной функциональности всего приложения.

Конечно, следует использовать модерацию и добавлять только тот код, который действительно необходим для всей программы.

Чтобы создать свое собственное промежуточное программное обеспечение (другими словами, свой собственный уровень), вам необходимо достичь следующих условий:

  • Находится в пакете Plack::Middleware::MYCOOLMIDDLEWARE, где MYCOOLMIDDLEWARE — это имя вашего промежуточного программного обеспечения.

  • Расширьте базовый класс Plack::Middleware (используйте родительский qw/Plack::Middleware/;).

  • Реализуйте вызов метода (функции).

Итак, реализуем простейшее Middleware с учетом всего вышеперечисленного:

package Plack::Middleware::PostOnly; use strict; use warnings; use parent qw/Plack::Middleware/; use Plack; use Plack::Response; use Plack::Request; sub call { my ($self, $env) = @_; my $req = Plack::Request->new($env); if ($req->method() ne 'POST') { my $new_res = $req->new_response(405); $new_res->body('Method not allowed'); return $new_res->finalize(); } return $self->app->($env); }

Давайте подробнее посмотрим, что произошло.

Есть код, который находится в пакете Plack::Middleware (пункт 1), который наследует базовый класс Plack::Middleware (пункт 2), реализует метод вызова (пункт 3).

Предоставленная реализация вызова делает следующее:

  • Принимает в качестве параметров экземпляр Plack::Middleware и env (my ($self, $env) = @_;).

  • Создает запрос, который принимает приложение (создание аналогично тому, которое использовалось в предыдущих примерах).

  • Проверяет, является ли метод запроса POST, если да, то промежуточное ПО пропускает дальнейшую обработку запроса.

Давайте посмотрим, что произойдет, если метод запроса не POST. Если метод не POST, создается новый объект Plack::Response, который немедленно возвращается без вызова приложения.

В целом функция вызова в Middleware может выполнять ровно 2 действия.

Этот:

  • Обработка env ПЕРЕД выполнением приложения.

  • Обработка результата ПОСЛЕ выполнения приложения.

Это будет показано в конце статьи, когда мы подведем итоги и разберемся в нюансах.



Использование Plack::Middleware и Plack::Builder вместе

Есть готовый слой Plack::Middleware::PostOnly, у нас есть приложение PSGI, у нас проблема.

Напомню, проблема выглядит так: «На данный момент мы не можем глобально влиять на поведение приложений».

Теперь мы можем.

Давайте рассмотрим самый важный момент Plack::Builder — ключевое слово Enable. Ключевое слово Enable позволяет вам подключить Plack::Middleware к вашему приложению.

Это делается следующим образом:

my $main_app = builder { enable "PostOnly"; mount "/" => builder { $api_app; }; }

Это очень простой и в то же время очень мощный механизм.

Давайте объединим весь код в одном месте и посмотрим на результат. Приложение PSGI:

use strict; use warnings; use Plack; use Plack::Builder; use Plack::Request; use Plack::Middleware::PostOnly; my $api_app = sub { my $env = shift; warn 'WORKS'; my $req = Plack::Request->new($env); my $res = $req->new_response(200); my $params = $req->parameters(); if ($params->{string} && $params->{string} eq 'data') { $res->body('ok'); } else { $res->body('not ok'); } return $res->finalize(); }; my $main_app = builder { enable "PostOnly"; mount "/" => builder { $api_app }; }

Промежуточное ПО:

package Plack::Middleware::PostOnly; use strict; use warnings; use parent qw/Plack::Middleware/; use Plack; use Plack::Response; use Plack::Request; sub call { my ($self, $env) = @_; my $req = Plack::Request->new($env); if ($req->method() ne 'POST') { my $new_res = $req->new_response(405); $new_res->body('Method not allowed'); return $new_res->finalize(); } return $self->app->($env); }

Приложение запускается следующей командой:

/usr/bin/starman --port 8080 app.psgi

Используемый код включает «PostOnly», поскольку Plack::Builder автоматически заменяет Plack::Middleware именем пакета.

Написано Enable «PostOnly», что означает включить «Plack::Middleware::PostOnly» (можно указать полный путь к вашему классу, используя + в качестве префикса, например, включить «+MyApp::Middleware::PostOnly»; – прим.

ред.).

Теперь, если вы пойдете по адресу локальный хост :8080/ используя метод GET, мы получим сообщение о том, что метод не разрешен с кодом ответа 405, а при доступе методом POST все будет хорошо.

Не зря в коде приложения присутствует строка предупреждения «РАБОТАЕТ».

Это подтверждает, что приложение не работает, если метод не POST. Попробуйте отправить GET, вы не увидите это сообщение в STDERR starman. Серверы PSGI имеют довольно много интересных поведенческих особенностей; о них обязательно пойдет речь в следующих статьях.

Давайте рассмотрим еще несколько полезных аспектов Plack::Middleware, а именно:

  • Результаты обработки ПОСЛЕ выполнения приложения.

  • Передача параметров в Middleware.
Допустим, есть два PSGI-приложения и вам нужно заставить одно работать через POST, а другое только через GET. Вы можете решить проблему, написав еще одно промежуточное программное обеспечение, которое будет реагировать только на метод GET, например, так:

package Plack::Middleware::GetOnly; use strict; use warnings; use parent qw/Plack::Middleware/; use Plack; use Plack::Response; use Plack::Request; sub call { my ($self, $env) = @_; my $req = Plack::Request->new($env); if ($req->method() ne 'GET') { my $new_res = $req->new_response(405); $new_res->body('Method not allowed'); return $new_res->finalize(); } return $self->app->($env); }

Проблема решена, но остается много дублирования.

Решение этой проблемы поможет вам наилучшим образом справиться со следующими вещами:

  • Механизмы передачи переменных в Middleware.
  • Подключение промежуточного программного обеспечения для приложений индивидуально.

Решение проблемы следующее: передать нужный метод как переменную.

Давайте вернемся к рассмотрению включения из Plack::Builder. Оказывается, Enable может принимать переменные.

Это выглядит так:

my $main_app = builder { enable "Foo", one => 'two', three => 'four'; mount "/" => builder { $api_app }; }

В самом промежуточном программном обеспечении доступ к этим переменным можно получить напрямую через $self. Например, чтобы передать значение переменной one, вам необходимо получить доступ к $self-> {one} в коде промежуточного программного обеспечения.

Продолжаем менять PostOnly. Пример:

package Plack::Middleware::GetOnly; use strict; use warnings; use parent qw/Plack::Middleware/; use Plack; use Plack::Response; use Plack::Request; sub call { my ($self, $env) = @_; my $req = Plack::Request->new($env); warn $self->{one} if $self->{one}; if ($req->method() ne 'GET') { my $new_res = $req->new_response(405); $new_res->body('Method not allowed'); return $new_res->finalize(); } return $self->app->($env); }

Перезапускаем starman, делаем запрос на localhost:8080, в STDERR видим следующее:

two at /home/noxx/perl/lib/Plack/Middleware/PostOnly.pm line 12.

Вот как переменные передаются в Plack::Middleware. Используя этот механизм, мы напишем Middleware, которое теперь будет называться Only.

package Plack::Middleware::Only; use strict; use warnings; use parent qw/Plack::Middleware/; use Plack; use Plack::Response; use Plack::Request; sub call { my ($self, $env) = @_; my $req = Plack::Request->new($env); my $method = $self->{method}; $method ||= 'ANY'; if ($method ne 'ANY' && $req->method() ne $method) { my $new_res = $req->new_response(405); $new_res->body('Method not allowed'); return $new_res->finalize(); } return $self->app->($env); } 1;

Теперь Middleware может отвечать только на метод запроса, переданный в параметрах.

Слегка измененное соединение выглядит так:

my $main_app = builder { enable "Only", method => 'POST'; mount "/" => builder { $api_app }; };

В этом случае приложение будет выполнено только в том случае, если методом запроса был POST. Давайте посмотрим на обработку результатов ПОСЛЕ выполнения приложения.

Допустим, если метод разрешен, в тело ответа следует добавить слово «ALLOWED».

То есть, если приложение должно вернуть ok, оно вернет ok ALLOWED, если, конечно, запрос не будет выполнен с использованием допустимого метода.

Приведем Only.pm к следующему виду:

package Plack::Middleware::Only; use strict; use warnings; use parent qw/Plack::Middleware/; use Plack; use Plack::Response; use Plack::Request; sub call { my ($self, $env) = @_; my $req = Plack::Request->new($env); my $method = $self->{method}; $method ||= 'ANY'; if ($method ne 'ANY' && $req->method() ne $method) { my $new_res = $req->new_response(405); $new_res->body('Method not allowed'); return $new_res->finalize(); } my $plack_res = $self->app->($env); $plack_res->[2]->[0] .

= 'ALLOWED'; return $plack_res; } 1;

$self-> app-> ($env) возвращает ссылку на массив из трех элементов (спецификация PSGI), тело которого модифицируется и возвращается в качестве ответа.

Чтобы убедиться, что все работает и работает как надо, можно передать переменные string=data и string=data1 разрешенным методом.

В первом случае, если метод разрешен, ответ будет иметь вид «okALLOWED», во втором — «not okALLOWED».

И напоследок давайте посмотрим, как именно можно объединить все вышеперечисленное в одно приложение Plak. Вернемся к исходной задаче.

Необходимо разработать простой API, который принимает строковую переменную и если строка=данные отвечает ок, в противном случае — нет, а также следует следующим правилам: При обращении/отвечаем любым способом.

При доступе к адресу /post отвечайте только на метод POST. При доступе к /get отвечайте только на метод GET. По сути вам понадобится ровно одно приложение, на котором написано — $api_app и немного модифицированный сборщик.

В итоге, используя все описанное выше, у вас должно получиться вот такое приложение:

use strict; use warnings; use Plack; use Plack::Builder; use Plack::Request; use Plack::Middleware::PostOnly; use Plack::Middleware::Only; my $api_app = sub { my $env = shift; my $req = Plack::Request->new($env); my $res = $req->new_response(200); my $params = $req->parameters(); warn 'RUN!'; if ($params->{string} && $params->{string} eq 'data') { $res->body('ok'); } else { $res->body('not ok'); } return $res->finalize(); }; my $main_app = builder { mount "/" => builder { mount "/post" => builder { enable "Only", method => 'POST'; $api_app; }; mount "/get" => builder { enable "Only", method => 'GET'; $api_app; }; mount "/" => builder { $api_app; }; }; };

Таким образом, соединение Middleware работает во вложенных маршрутах Plack::Builder. Стоит обратить внимание на простоту и логичность кода.

Отложенный ответ будет рассмотрен в одной из статей, посвященных асинхронным серверам (Twiggy, Corona, Feersum).

Теги: #perl #psgi #веб-разработка #perl #разработка веб-сайтов

Вместе с данным постом часто просматривают: